Deploy a MEAN app with Nginx and PM2

This is a short summary of how to deploy a MEAN app (MongoDB, Express, Angular, and Node.js) on Nginx and run the app with PM2.

Install Node/NPM on a production server

curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.33.9/install.sh | bash` <br> This will add a source line to your profile. <br>`export NVM_DIR="$HOME/.nvm"
[ -s "$NVM_DIR/nvm.sh" ] && \. "$NVM_DIR/nvm.sh" # This loads nvm`. Check your `~/.bashrc

NVM can install multiple node versions. This is good considering that your app can be updated to use a different version of node in the future. Remember that NVM is installed per a user account. For example, if you run an app using wwwuser user account, NVM must be installed under wwwuser login. NVM is located in /home/wwwuser/.nvm. Using NVM command (nvm run node --version), Node is installed under /home.wwwuser/.nvm/versions/node/v8.11.1. NPM will be installed together when you install a node version. Just for information, there are other ways of installing node/npm: 1) using Linux distribution packages and 2) Nodesource. The first one can be a little outdated, because the package can take a little time to include the latest editions. The second one is a good choice for update the latest. However, I decide to choose NVM method to manage multiple node/npm versions.

Git Hook

Now you are ready to move your application files onto the server. Create a git hook. Git is a useful tool for code backup and sharing. However, Git can be also used for deployment with the help of git hooks. See how to create a git hook here. Using a git for deployment is just as simple as you type a git command (git push production master). All code deployment will be taken care of by a post-receive hook which will be set up on the 'bear' git repository. Now we put all our codes on the deployment folder. Go to the folder, run npm install --production, which will prepare all node modules for your app. Now your files are all ready. But you need to setup a back-end storage where your dynamic contents interact with.

Setup MongoDB

When a node application starts, the app will connect to a MongoDB instance. For example, the first thing may be to setup/register the first app user account. So, you must prepare database first before starting an app. This is an example of how to install MongoDB on Red Hat Enterprise or CentOS Linux.

  1. Create a /etc/yum.repos.d/mongodb-org-3.6.repo file so that you can install MongoDB directly using yum. The details of the file content is here.
  2. Run sudo yum install -y mongodb-org. This will install MongoDB community edition.
  3. Before using MongoDB, we need to create user accounts. MongoDB has a role-based access control, which means that only a user given a permission have access to a resource. We need two accounts initially: a user administrator and an application user. First, connect to MongoDB using mongo. And you can now use useful commands in console. Let's create a user administrator.
  4. It would be cool if MongoDB automatically starts after system reboot. You do not know when a Linux production server can die. To enable MongoDB on every system reboot, run a command sudo systemctl enable mongod.service. To disable it, run sudo systemctl disable mongod.service. For checking MongoDB service status, use systemctl status mongod.
  5. Everything is ready. Run sudo service mongod start to start MongoDB service. Play with sudo service mongod stop and sudo service mongod restart.
        $ use admin
        switched to db admin
        $ db.createUser(
            {
                user: "mongo-admin",
                pwd: "abc123",
                roles: [ { role: "userAdminAnyDatabase", db: "admin" } ]
            }
         )
        Successfully added user: {
	        "user" : "mongo-admin",
	        "roles" : [
		        {
			        "role" : "userAdminAnyDatabase",
			        "db" : "admin"
		        }
	        ]
        } 

We will do the same thing for an application user. But now we have to login using the account ('mongo-admin') that we just created the second user.
$ mongo --port 27017 -u "mongo-admin" -p "abc123" --authenticationDatabase "admin"

You are now loginned with a user administrator. Run the following command.

        $ use sierra-app
        switched to sierra-app
        $ db.createUser(
          {
            user: "sierra-user",
            pwd: "123xyz",
            roles: [
              { role: "readWrite", db: "sierra-app" }
            ]
          }
        )

Now your application can communicate with MongoDB database (sierra-app) with the code line, mongodb://sierr-user:123xyz@localhost/myappdb.

Almost done. there is one more important thing, which is a configuration of /etc/mongod.conf. Open the file and update dbPath and security accordingly. My dbPath is /var/lib/mongo. This folder will contained all data stored in MongoDB. Uncomment the line 'security' and add authorization: enabled. By default, MongoDB is accessible without user login. It is not ideal for the production. The security setup will require username/password to access to MongoDB. Finally, you can apply the change with sudo service mongod restart.

Test your node application. Go to your deployment folder and run node server.js. (starting command can be varied depending on your application setup). You will find that your app is running. However, in production, we want to serve the app with Nginx with a reverse proxy, because it can handle static files better.

Setup Nginx

A MEAN app uses Angular for the front-end and Express for the back-end. So, I want to serve both parts on different locations in a Nginx server. Inside a server block, create two locations: the one serving the front app and the other serving the back-end app. The first one will be mapped to the absolute path to the front-end file location (alias /var/www/sierra-app/front;). Notice that the server location is /sierra-app. This means that your app will be hosted on http://sierra-server.org/sierra-app/. For an Angular app, you must build the app with ng build --base-href /sierra_app/. Remember that you put /sierra-app/index.html for the last value of try_files, which will make a single-page Angular application reload its routes without hash (#) in url. Html5 mode does not require the hash. However, you have to serve the application route on the server-side. Secondly, the back-end location will use a reverse proxy and get a proxy pass to 3000 port, on which an express application will run (proxy_pass http://localhost:3000/api;). Because the express app have all routes under /api directory, the nginx location /sierra-app/api will work as naturally as it is like being located under /sierra-app.

//etc/nginx/conf.d/virtual.conf
            server {
            listen 80;
            server_name sierra-server.org;
            location / {
                root /var/www/html;
                index index.html index.htm;
            }
            location /sierra-app {
                alias /var/www/sierra-app/front;
                index index.html;
                try_files $uri $uri/ /sierra-app/index.html =404;
            }
            location /sirra-app/api {
                proxy_pass http://localhost:3000/api;
                proxy_http_version 1.1;
                proxy_set_header Host $host;
                proxy_set_header X-Real-IP $remote_addr;
                proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
                proxy_set_header X-Forwarded-Proto $scheme;
                proxy_set_header X-NginX-Proxy true;
                proxy_set_header Upgrade $http_upgrade;
                proxy_set_header Connection "upgrade";
                proxy_cache_bypass $http_upgrade;
                }
            }

Ok. Now restart nginx with a command sudo service nginx restart. And go to http://sierra-server.org/sierra-app. It will open the front app. And restart your node app with node server.js. Try requesting data from the back-end. It will pull data from the backend to your browser. This means that the back-end express app is running OK on the nginx location, /sierra-app/api.

    There are a few useful Linux commands:
    > sudo adduser username
    > sudo passwd username
    > sudo groups username
    > sudo vi /etc/group
    > sudo usermod -aG wheel username (add to sudoers)
    > sudo gpasswd -d username groupname (delete a user from group) 
    > su - appuser
    > sudo chown -R appuser:appuser foldername/
    > scp original.txt appuser@sierra_server.org:/var/www/sierra_app
    > scp appuser@sierra_server.org:/var/www/sierra_app ./sierra_app


There are some more tuning for nginx server.

* gzip encoding
* Http caching
* SSL handling

Finally, we will run the back-end express app with PM2. PM2 can restart the back-end app when the app is crashed. And the app can be auto-start when a server get crashed and rebooted. On top of that, PM2 monitors and manages multiple node processes. To install it, run npm install pm2@latest -g. This will install pm2 under your user login, more specifically (/home/username/.nvm/versions/node/v8.11.1/bin). PM2 can start an app with pm2 start server.js. To auto-start PM2 after a system reboot, run pm2 startup to create a startup script (MUST run it as a root using sudo). The script will be stored in /etc/systemd/system/[pm2-wwwuser.service]. You may save the current processes for the next start, pm2 save. The process list will be saved into /home/username/.pm2/dump.pm2. When the pm2 startup service restarts pm2, it will automatically resurrect all processes listed in dump.pm2.