Перейти к содержимому

Sitecore JSS: a blue green Node.js server configuration

The target of this post is to share how to configure headless with a .js proxy on and to demonstrate a staging approach for the hosting of the CD role. Since the front-end is rendered on a stand-alone Node proxy, we can easily configure as much backend CD servers as needed, which allows us to deploy CD code on a staging server, and then switch the target server on the Node instance.

Node.js JSS server configuration on CentOS – instructions and scripts

This part will show you an example of how you can configure your Node.js SSR server from scratch. For this I assume that you have basic knowledge of working in .

wget linkGoesHere

Go to your local users home and download the required Node.js release (replace linkGoesHere in the following script with the link from https://nodejs.org/en/download/ ):

mkdir node
tar xvf node-v*.tar.xz --strip-components=1 -C ./node
rm -rf node-v*

Create a directory, unpack the downloaded archive and remove it afterwards:

mkdir node
tar xvf node-v*.tar.xz --strip-components=1 -C ./node
rm -rf node-v* 

Instruct npm where the symlinks shall be put:

mkdir node/etc
echo 'prefix=/usr/local' > node/etc/npmrc

Move Node.js to /opt and create the required symlinks to execute it:

sudo mv node /opt/
sudo chown -R root: /opt/node
sudo ln -s /opt/node/bin/node /usr/local/bin/node
sudo ln -s /opt/node/bin/npm /usr/local/bin/npm

When a command is executed with sudo, /usr/local/bin is excluded from the path. So, we need to add this path to the allowed paths (to have the possibility to call node and npm with sudo):

sudo visudo
#Find the line that specifies Defaults secure_path and add :/usr/local/bin to the end of it. 
#should look like 
#Defaults secure_path = /sbin:/bin:/usr/sbin:/usr/bin:/usr/local/bin

We've just finished the installation of node.js . To verify this, execute "node -v", which will show you the installed Node.js version.

To manage the SSR proxy and keep it up and running, we need a Node.js process manager. * world has a lot of them, so it can be difficult to pick one. I chose to use pm2, because it is simple, easy to use and has an option to either watch or ignore file system changes.

So let's install and daemonize pm2:

sudo npm install pm2@latest -g
#I want daemonize node app via current user, not root
#follow instructions of command
pm2 startup systemd

After that, pm2 will start its service under the current user.

Since Node.js apps is not big in security and caching (without special precautions), and setting up SSL on them is also quite a challenge, I will put Nginx in front of it to listen for requests on ports 80 and 443, and send them to my SSR proxy.

To install Nginx, execute the following script:

sudo yum install epel-release
sudo yum install nginx
sudo /usr/sbin/setsebool httpd_can_network_connect 1

And ensure that Nginx does not expose its version to improve system security (we should disclose a minimum amount of data about our systems):

sudo vi /etc/nginx/nginx.conf
#Then, inside the  configuration part add the line server_tokens off; like this:
http {
		server_tokens off;

Then, configure SSL:

sudo mkdir /etc/nginx/ssl/
sudo openssl dhparam -out /etc/nginx/ssl/dhparam.pem 4096

Put your certificate and key into "/etc/nginx/ssl" and note the paths.
Subsequently, restore the security configuration for the SSL certificates:

restorecon -v -R /etc/nginx/ssl

And configure your application. Nginx will redirect all incoming requests on port 80 to port 443; on port 443, after a successful TLS handshake, all requests will be deciphered and sent to the Node.js app, hosted in pm2 and listening on port 3000:

sudo touch /etc/nginx/conf.d/app.conf
sudo vi /etc/nginx/conf.d/app.conf

server {
listen 80 default_server;
listen [::]:80 default_server;
server_name _;
return 301 https://$host$request_uri;
server {
listen 443 ssl http2 default_server;
listen [::]:443 ssl http2 default_server;
server_name _;
root /usr/share/nginx/html;
ssl_protocols TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_dhparam /etc/nginx/ssl/dhparam.pem;
ssl_certificate /etc/nginx/ssl/YOURCERTIFICATE;
ssl_certificate_key /etc/nginx/ssl/YOURKEY;
location / {
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection 'upgrade';
proxy_set_header Host $host;
proxy_cache_bypass $http_upgrade;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Host $host;
proxy_set_header X-Forwarded-Server $host;
proxy_set_header X-Forwarded-Port 443; proxy_set_header X-Forwarded-Proto $scheme;

Now check the configuration, restart Nginx and enable it:

sudo nginx -t
#if there is errors here – fix them
sudo systemctl restart nginx
sudo systemctl enable nginx

Clone the Sitecore SSR proxy example and prepare for hosting it in PM2:

#SSR proxy configuration
git clone https://github.com/Sitecore/jss.git
sudo mkdir /opt/node-app/
sudo chown YOURUSER: /opt/node-app/
cp jss/samples/node-headless-ssr-proxy/** /opt/node-app/
cd /opt/node-app/ && npm install

At this point, the application is still not functioning, but all further configuration will be done on VSTS.

VSTS release configuration to deploy to staging server and then switch it

My release pipeline consists of several stages, but this blog post will not cover it all, as it would take a lot of time. Instead, I will concentrate on explaining how to do "green/blue" deployments for JSS.

The main tools used during a release are an SSH connection to deliver changes to the CentOS host and MsDeploy to deliver changes to IIS.

Our configuration is pretty simple: we are running on Sitecore 9.0.2 with JSS; our infrastructure consists of 1 CentOS webserver for the Node.js SSR proxy, 3 Windows servers (1 for CM, 1 for xConnect and 1 for the CD role). On the CD server I've configured 2 IIS websites, which names are the same as internal hostnames (cd1.host.local and cd2.host.local).

CD and Node.js delivery stage

The first step of this stage is to check the Node.js proxy configuration, to understand on which server we should deliver our code now (so we are deploying to a website which is not under load ATM). It is done by connecting to our Node.js webserver and grabbing the hostname from the config (and since the hostname is the same as the IIS site name, that's all I need from there). Then this value is stored in a variable to be reused by subsequent steps:

currentHost=`grep -A0 'apiHost' $(nodeApp.Path)/config.js | grep -oP "http(s)?:\/\/(.*)'"|sed 's/https\?:\/\///' | sed "s/\(.*\).\{1\}/\1/"`
echo "##vso[task.setvariable variable=deploy.currSite;]$currentHost"
deploymentHost=`[[ "$currentHost" == "cd1.host.local" ]] && echo "cd2.host.local" || echo "cd1.host.local"`
echo "##vso[task.setvariable variable=deploy.iisSiteName;]$deploymentHost"

The variable "nodeApp.Path" contains the value "/opt/node-app", which is where we've deployed the node-headless-ssr-proxy sample from https://github.com/Sitecore/jss.git to, and the "deploy.iisSiteName" variable will now be holdingthe name of the target website (the one which is not under load). And then, even at first run, this script will not fail.

"$(msdeploy.Path)" -allowUntrusted="True" -verb:sync -source:recycleApp -dest:recycleApp="$(deploy.iisSiteName)",computerName="https://$(deploy.targetComputer):8172/msdeploy.axd?site=$(deploy.iisSiteName)",recycleMode="StopAppPool",username="$(user)",password="$(password)",authType="Basic"
"$(msdeploy.Path)" -allowUntrusted="True" -enableRule:DoNotDeleteRule -verb:sync -source:package="%sourcePath%" -dest:contentPath="$(deploy.iisSiteName)",computerName="https://$(deploy.targetComputer):8172/msdeploy.axd?site=$(deploy.iisSiteName)",username="$(user)",password="$(password)",authType="Basic"
"$(msdeploy.Path)" -allowUntrusted="True" -verb:sync -source:recycleApp -dest:recycleApp="$(deploy.iisSiteName)",computerName="https://$(deploy.targetComputer):8172/msdeploy.axd?site=$(deploy.iisSiteName)",recycleMode="StartAppPool",username="$(user)",password="$(password)",authType="Basic"

The next steps will stop the app pool, deploy a prepared application package and start it (to allow the CD server to preheat itself with an application init) via MsDeploy with the following commands:

The next step is to update the Node.js app on the CentOS host. /dist/app content is prepared during web app compilation, so here we only need to update config.js with the new hostname. I am using https://github.com/qetza/vsts-replacetokens-task#readme to replace the token "deploy.iisSiteName" in config.js.

if [ -d $(nodeApp.dist.path) ]; then rm -Rf $(nodeApp.dist.path); fi

As soon as the files are ready to upload, I execute the following SSH script to cleanup the target path:

"nodeApp.dist.path" can contain a unique value, generated from the static app name and the release number for example.

Then I will deploy the files to the CentOS host via the Copy files Over SSH task and restart the pm2 application:

cd $(nodeApp.Path)
#see https://github.com/Unitech/pm2/issues/325#issuecomment-281580956 for explanation
pm2 delete -s $(nodeApp.Name) || :
pm2 start index.js --name $(nodeApp.Name)

At this point, our Node.js SSR proxy is pointing towards the newly updated IIS website. It then could be wise to stop the app pool of the 'old' CD IIS website with the following command:

"$(msdeploy.Path)" -allowUntrusted="True" -verb:sync -source:recycleApp -dest:recycleApp="$(deploy.currSite)",computerName="https://$(deploy.targetComputer):8172/msdeploy.axd?site=$(deploy.currSite)",recycleMode="StopAppPool",username="$(user)",password="$(password)",authType="Basic"

And that's it. You have now created a simple and effective blue green setup for your headless Sitecore JSS website using a Node.js proxy and multiple CD role servers to do the swapping.

This blog post have been created by me and edited by my colleague and friend Rob Habraken