Setup Node&Mongo API on Ubuntu
In this post I will go through the steps on how to configure a very simple setup with test and production environments for a Node api on an Ubuntu server.
The main idea is to run a single server that exposes two instances of the api - one for test and one for production purposes. Then the consuming application can use the test version of the api under development and the production one in production environment.
What we are going to do here in a nutshell: install MongoDB
and create test and production databases, install Node.js
, fetch the api on the server machine, install process manager pm2
and run it on the api, install and configure nginx
as a reverse proxy for pm2
. In the end there will be test and production versions of the api sitting on the same domain but different url paths and they will be automatically started in case of expected/unexpected server reboot.
This setup isn’t meant for a large “production”-like project but it worked really well for my small hobby project which is a Node.js
API in GraphQL
and Express
and a MongoDB
database. So if you have a small project with a similar stack of technologies and looking for a quick and simple setup you might want to stick around here for a while :)
Before we begin configuring the server we need to have the api switch connection strings to the db depending on the environment and be able to run the api on different ports. This is a typical use-case for environmental variables.
Configure environmental variables in the api
Environmental variables in Node
are accessed via process.env
object. So if we want to pass port as an env variable we will use it like this:
const PORT = process.env.PORT || 4000;
app.listen(PORT, () => {
console.log(`Server is listening on port ${PORT}`);
});
Now we need a way to have different connection strings to the mongo
db depending on the value of NODE_ENV
which will be set to development
or production
. We will set those connection strings as project’s specific environmental variables in .env
file and load them into process.env
using an npm
package dotenv
. So first install the package:
npm install --save dotenv
and create .env
in the root of the project with two connection strings, for example:
MONGO_URL_DEV="mongodb://localhost:27017/my-db-development"
MONGO_URL_PROD="mongodb://localhost:27017/my-db-production"
Your .env
might contain very sensitive information (like username/password to the db) so it is extremely important to add it to .gitignore
. Just add a line with .env
to your .gitignore
and you are safe :)
According to the dotenv docs, this line should appear as early as possible in the code:
require('dotenv').config();
so we will have it as first line in the file that initializes the node server (usually index.js
, server.js
or app.js
).
To read the connection strings we can now access process.env.MONGO_URL_DEV
and process.env.MONGO_URL_PROD
. We want to use the correct url depending on the value of NODE_ENV
and to encapsulate this logic we can create a config
file or add it to the existing config
:
module.exports = {
development: {
url: process.env.MONGO_URL_DEV
},
production: {
url: process.env.MONGO_URL_PROD
}
};
And fetch the right connection string where we establish connection to the database:
const nodeEnv = process.env.NODE_ENV || 'development';
const { MongoClient } = require('mongodb');
// select the configuration for the current node environment
const mongoConfig = require('../config/mongo')[nodeEnv];
MongoClient.connect(mongoConfig.url, (err, mPool) => {
// express code: define routes, headers etc.
...
});
If you want to check how this is implemented in my api, check the full code on my github.
The rest of the time we are going to spend in the terminal one-by-one with the ubuntu server :)
Connect to the server
You should already have a server with a clean Ubuntu
installed and open ports for HTTP
and SSH
connections. The actual steps in this post were performed on an Azure
virtual machine running Ubuntu 18.04
(in case of creating a new virtual machine on Azure
there is a step allowing to select public inbound ports of which HTTP
and SSH
should be selected). In the terminal write:
ssh username@ip-address
enter correct password
Phew.. the toughest part is now behind. Get the system updates:
sudo apt-get update
Install and start MongoDB
There is an official guide on how to install the MongoDB
packages on Ubuntu
available on mongodb docs, some of the steps have specific commands depending on the version of Ubuntu
.
After you have completed all the steps, start mongo with
sudo service mongod start`
and make sure it is running by issuing:
sudo service mongod status
To make mongo
wake up on system startup run:
sudo systemctl enable mongod.service
Create test and prod databases
Let’s connect to mongo
shell and check what databases it has:
mongo
show dbs
Now create a test db and collections:
use my-db-development
db.createCollection("things")
Should return {"ok": 1}
Create a production db and collections:
use my-db-production
db.createCollection("things")
Once again should say {"ok": 1}
Type exit
and let’s go on to the next step.
Install and configure Node.js
If you try simply running sudo apt install nodejs
, it will leave you with some really old version of node
, so this path is not recommended. At the time of writing the latest stable version of node
is 8.11.4 (includes npm 5.6.0) and it can be installed from a NodeSource
binary distribution.
Go to home directory with cd ~
and get the binaries (the steps borrowed from this nodejs documentation):
curl -sL https://deb.nodesource.com/setup_8.x | sudo -E bash -
sudo apt-get install -y nodejs
Verify that running nodejs -v
gives the latest version of node.js
.
Setup the api
Install Git
:
sudo apt-get install git
and confirm it with git --version
.
Navigate to your user’s home directory and create a folder for your api:
cd ~
mkdir my-things-api
cd my-things-api
Clone the repository into two locations development
and production
.
git clone https://github.com/your-username/my-things-api.git development
git clone https://github.com/your-username/my-things-api.git production
Now, under development
you might want to git checkout
your dev
branch and under production
your master
branch. This depends on the git workflow you have chosen for your project.
Run npm install
in each folder and create a .env
file with touch .env
. Open the file with your favorite text editor (vim .env
) and paste the connection strings we created previously into the file, for example:
MONGO_URL_DEV="mongodb://localhost:27017/my-db-development"
MONGO_URL_PROD="mongodb://localhost:27017/my-db-production"
Now we can start and stop the express
server just to see that it doesn’t crash:
cd ~/my-things-api
NODE_ENV=production node production/path_to_the_server/app.js
NODE_ENV=development node development/path_to_the_server/app.js
Install process manager pm2
Install pm2
and set it to wake up on server restart:
sudo npm install -g pm2
pm2 startup
Run the command generated by startup
to finish startup configuration.
Let’s create a configuration file for pm2
with two app instances which have NODE_ENV
set to development
and production
. Generate a default file in the home directory with the api and edit it to set our configuration:
cd ~/my-things-api
pm2 ecosystem
vim ecosystem.config.js
Here is the configuration I used for the setup:
module.exports = {
apps : [{
name : 'my-things-api',
script : './development/path_to_the_server/app.js',
watch: true,
env: {
NODE_ENV: 'development'
}
},{
name : 'my-things-api',
script : './production/path_to_the_server/app.js',
watch: true,
env: {
NODE_ENV: 'production',
PORT: 4010
}
}]
};
It is extremely simplistic and you might want to add paths to error_file
and out_file
to log errors and output, more attributes can be found in the pm2 docs.
Start pm2
and check the list of processes it started:
pm2 start ecosystem.config.js
pm2 list
Two instances of the api should now be running. To check run:
curl http://localhost:4000
curl http://localhost:4010
Finally save the started applications so that they can be restarted on server restart
pm2 save
At this point I started thinking about setting up Jenkins
to poll the repository for changes (or using github hooks) to automatically deploy new versions of the development and production branches, but that felt like too much for my small project. You are free to investigate this topic and tell about the consequences :)
Install and configure nginx
We will use nginx
as a reverse proxy that will accept requests from the network and pass them down to the internal api running on nodejs
and managed by pm2
.
A good guide on how to install nginx
is available on digital ocean. Or follow those steps:
sudo apt install nginx
sudo ufw allow 'Nginx HTTP'
sudo ufw status
sudo systemctl status nginx
Now let’s enable nginx on system startup
sudo systemctl enable nginx
To configure nginx to act a reverse proxy we need to add some configuration. For simplicity we will edit the default nginx
configuration file:
sudo vim /etc/nginx/sites-available/default
Let’s assume you want to access your api from /things-api/development
and /things-api/production
. In this case add the following configuration to the file after the first location
section:
location /things-api/development/ {
proxy_pass http://localhost:4000/; # note the trailing slash!
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
}
location /things-api/production/ {
proxy_pass http://localhost:4010/; # note the trailing slash!
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header Host $http_host;
proxy_set_header X-NginX-Proxy true;
}
It is extremely important to have the trailing slash in proxy_pass
since this will make nginx
pass the rest of the url path after the api path in your node application.
You might also want to disable the default nginx
start page, which can be done by editing the section with location /
:
location / {
return 404;
}
All the rest in the config file is intact. Save and close the file.
Check whether syntax in the config file is correct and restart:
sudo nginx -t
sudo systemctl restart nginx
Time to log out, run exit
.
Final test
The api should now be running under http://server-domain-name/things-api/development/
and http://server-domain-name/things-api/production/
and you should be able to test it in the browser or postman
.
If you are brave enough, restart the server and check that everything is going to start up correctly after the reboot :)