Deploy Angular Universal App to Shared Apache Hosting (With Passenger)
(Last Updated On: March 19, 2023)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:
- Use Angular Universal’s Prerender to deploy an static website.
- 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.
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.
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
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ú 🐱🐶.
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 »
My htaccess looks like this: # 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 SetEnv CONTACT_EMAIL_ADDRESS [email protected] SetEnv CONTACT_EMAIL_PASSWORD some-random-password-here # DO… Read more »
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.
This is weird. Did you tried locally with SSR active?
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.””
This is a node error. Maybe your shared hosting doesn’t have enough shared CPU or memory?
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.
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 »
You have to see what Phussion logs say. Otherwise it is impossible to know what is happening.
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 »