Deploy Angular Universal App to Shared Apache Hosting (With Passenger)

/ March 15, 2022/ Angular

(Last Updated On: March 19, 2023)
Ad:

If we search for “How to deploy Angular Universal to production” or “How to deploy Angular Universal to server”, we will find a lot of examples of one of two scenarios:

  1. Use Angular Universal’s Prerender to deploy an static website.
  2. Deploy an Angular Universal App to a VPS or a nginx server.

The thing is, most of us that already have some kind of website, already have a hosting provider, and most likely, it is an Apache-based shared hosting.

The cool thing is that Apache-based hostings have the ability to deploy Node (and another web) apps in a couple of clicks using Passenger, the not-so-cool thing is that an Angular Universal production build does not work out of the box there, and according to several people, it just plain not work.

In this post, I will write a step-by-step guide on how to deploy your Angular Universal App in a painless way. It will just take 10 minutes! (well maybe 20).

Step 1: Create an Node App on cPanel.

Ad:

On your hosting cPanel’s search bar write: “node.js”. You should find a “Setup Node.js App” option there. If not, then your hosting does not have Passenger installed and you can not host node apps there 😢

Inside there, click Create Application. Here just fill the Application root, and choose the Application URL.

After this is done, Passenger will generate an app.js, and a .htaccess file inside the Application root folder. Click on Start app and in your browser, navigate to the Application URL. You should see a message like this:

It works!

NodeJS 14.18.3

Edit:

If Passenger does not generate you an .htaccess file, this is how it looks like mine.

# DO NOT REMOVE. CLOUDLINUX PASSENGER CONFIGURATION BEGIN
  PassengerAppRoot "/home/my-user/some-folder/my-website.com/production"
  PassengerBaseURI "/"
  PassengerNodejs "/home/my-user/nodevenv/some-folder/my-website.com/production/14/bin/node"
  PassengerAppType node
  PassengerStartupFile dist/my-angular-project/server/main.js
  # DO NOT REMOVE. CLOUDLINUX PASSENGER CONFIGURATION END
  # DO NOT REMOVE OR MODIFY. CLOUDLINUX ENV VARS CONFIGURATION BEGIN
  <IfModule Litespeed>
     SetEnv CONTACT_EMAIL_ADDRESS [email protected]
    SetEnv CONTACT_EMAIL_PASSWORD some-random-password-here
  </IfModule>
  # DO NOT REMOVE OR MODIFY. CLOUDLINUX ENV VARS CONFIGURATION END

Step 2: Build your Angular Universal app.

Ad:

By default, our Angular Universal app server runs on port 4000, but, our Apache/Passenger host serves our site in port 80 for HTTP and port 443 for HTTPS. Since we are on shared hosting, there is no control over the server settings. This means we can not do any kind of port mapping or reverse proxy.

If you are like me, you are thinking “OK, then I will change the Angular port”. Sadly this will just produce an EACCESS error saying that you do not have permission to use that port(s).

Luckily, a post on the plesk board gave us the fix for this problem: we do not need to set any port, Passenger will take care of passing the server request to your node app.

Another problem we will face is that the run() function inside server.ts will not be called in our server because it is wrapped in an if statement that is never true. I have already created a pull request to fix this on the Universal repo. For now let’s us fix it locally.

Changing server.ts

Replace this:

function run(): void {
  const port = process.env['PORT'] || 4000;

  // Start up the Node server
  const server = app();
  server.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:${port}`);
  });
}

With this:

function isRunningOnApachePassenger(): boolean {
  return moduleFilename.includes('lsnode.js');
}

function run(): void {
  // Start up the Node server
  const server = app();

  if (isRunningOnApachePassenger()) {
    server.listen(() => {
      console.log('Node Express listening to Passenger Apache');
    });
    return;
  }

  const port = process.env['PORT'] || 4000;

  server.listen(port, () => {
    console.log(`Node Express server listening on http://localhost:${port}`);
  });
}

and this:

if (
  moduleFilename === __filename ||
  moduleFilename.includes('iisnode')
)

with this:

if (
  moduleFilename === __filename ||
  moduleFilename.includes('iisnode') ||
  isRunningOnApachePassenger()
)

Build your project

npm run build:ssr

This will leave you with this folder structure

dist
  /your-app-name
    /browser
    /server
dist is the folder that we will upload later to our hosting.

Step 3: Upload your files

Ad:

Since we do not want to complicate our lives, we will upload all the dist folders to our host inside the Application root folder from Step 1.

We should have something like this at the end:

server/path/to/your/application-root
  /dist
    /your-app-name
      /browser
      /server

Final step, update your Node.js app in cPanel

We have to change the Application startup file value inside our App config in cPanel, which currently is app.js to:

dist/your-app-name/server/main.js

Now, click on Restart and that is all! 🚀

I did this to successfully deploy the (for now hidden) future website of MascotasJesusMaria.com, my brother’s website about pets in Perú 🐱🐶.

Spread the love
Subscribe
Notify of
guest
10 Comments
Oldest
Newest Most Voted
Inline Feedbacks
View all comments
Dmitrii
Dmitrii
1 year ago

Hey! I faced the same problem as you, but your fix didn’t help me. Passenger gives me error that server didn’t respond at startup. I feel like the condition to call the run() method still isn’t met. Wasn’t something changed during last year? Is this still a valid workaround? What’s… Read more »

Carlos
Carlos
Reply to  Saninn Salas Diaz
1 year ago

I get it to work the first time but if I refresh it… it gives me a 404. but any route I type in browser works… if it pre-exists.

Jalil
Jalil
1 year ago

Hello !
i do all the think in your tuto but i get error
“”Could not spawn process for application /home/c2154511c/public_html/myApp: A timeout occurred while spawning an application process.””

Houssem eddine aouinti
Houssem eddine aouinti
Reply to  Jalil
9 months ago

As Sannin Salas Diaz mentioned, I faced the same problem initially: server response time exceeding 30 seconds. I discovered that my CPU usage was consistently at 100%. After upgrading my shared hosting performance, the issue was resolved.
PS: Focusing on prerendering entry points and critical routes made crawling easier.

Jalil
Jalil
1 year ago

Hello Thank You u for your help I have done every think again and again like your tutorial And I use only the basic angular sample (initial angular generate app). when I use SSH terminal to launch my application, it run and I can open the application using app url.… Read more »

passenger
Jalil
Jalil
Reply to  Saninn Salas Diaz
1 year ago

I don’t know why, but I use this code in server.js and it is ok nown. I add moduleFilename.includes('node-loader.js') in if condition of run(). .... function run(): void {   // Start up the Node server   const server = app();   if (isRunningOnApachePassenger()) {     server.listen(() => {… Read more »