Kevin Crawford, Software Engineer

Front-End Specialist · Full-Stack Generalist

Configuring a Production Node.js and MongoDB Environment in Ubuntu on an Amazon EC2 Instance

This tutorial will cover launching an EC2 instance, setting up the Node.js/MongoDB stack, and keeping your app running as a service so that it is resilient to failure. Most everything is taken directly from the official documentation for the various packages, and I included links. That way you can refer to the tutorial as a general guide, and still use official documentation to get into the nitty-gritty details.

Right, then. Let's get started!

First, a Note About Keypairs

When you launch a new instance, it's pretty funky that Amazon doesn't let you paste or upload your public key to use for authentication. You can either generate a new private key to download, or select an existing key from your account. I recommend that you upload your public key beforehand, by going to the AWS Console -> EC2 -> Key Pairs -> Import Key Pair.

You can also upload a public key via the command line, with ec2-import-keypair. I installed aws-cli (note: slightly different from ec2-import-keypair) and issued the following command:

1
aws ec2 import-key-pair --key-name user@email.com --public-key-material file://~/.ssh/id_rsa.pub

If you do this beforehand, your key will show up in the web interface for you to select when launching. Otherwise, AWS will generate the private key for you to download.

Provision your EC2 instance

I chose Ubuntu Server 13.10 for my AMI. You'll want to use 64-bit for optimal MongoDB support (see this post). Make sure to read through all the options in the wizard to configure your instance for your needs. MongoDB recommends an instance type that is EBS-optimized. More on that shortly.

If you use an automatically assigned IP address, you'll lose it when your instance is stopped or terminated. So, to prevent any DNS-related interruption of service, you'll want to reserve a dedicated IP address, which Amazon refers to as an Elastic IP (EIP). You can't assign an EIP until you've already launched your EC2, so we'll revisit that later. For right now, automatically assign a public IP.

Storage

The AWS storage documentation recommends using EBS for persistent data that you care about–like your database. From the documentation:

An Amazon EBS volume behaves like a raw, unformatted, external block device that you can attach to a single instance. They persist independently from the running life of an Amazon EC2 instance. After an EBS volume is attached to an instance, you can use it like any other physical hard drive. As illustrated in the previous figure, you can attach multiple volumes to an instance. You can also detach an EBS volume from one instance and attach it to another instance.

This comes in handy if you need to upgrade your EC2 instance, or provision a new one for any reason. Again, MongoDB recommends an EBS-optimized EC2 instance with a Provisioned IOPS EBS volume. So set up an EBS volume, and we'll be mounting it for MongoDB to use. It would make good sense to utilize Amazon S3 for static assets, but that's outside the scope of this article.

Configure Security Group

You'll want to have SSH (port 22) open to any IP you might access it from. You'll also want HTTP and HTTPS ports accessible from any IP.

Launch!

You didn't upload your SSH key beforehand, did you? That's okay, I didn't figure that out until afterwards, either. So I generated a key, and copied my normal SSH key into the authorized_keys file afterwards. Assuming you've placed the key in your ~/.ssh/ dir:

1
2
3
4
5
6
7
8
# Make sure you restrict permissions for your private key file
chmod 400 ~/.ssh/aws.pem
# Add the downloaded AWS key to your key agent
ssh-add ~/.ssh/aws.pem
# If you don't have ssh-copy-id, you can install it via homebrew:
brew install ssh-copy-id
# Then, copy your normal SSH key to the EC2's ~/.ssh/authorized_keys file
ssh-copy-id -i ~/.ssh/id_rsa.pub ubuntu@hostname

Now you can setup ssh agent forwarding for handy stuff like connecting to Github!

Onwards: Install and Configure MongoDB

Again, this is all based on MongoDB's official documentation. They recommend using separate EBS stores for your data, journal, and log, but I'm just going to cover putting it all on a single EBS to keep things a little simpler.

First, let's install MongoDB. These commands are directly from their tutorial:

1
2
3
4
5
6
7
8
# Import MongoDB's GPG key used to ensure package authenticity
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 7F0CEB10
# Create a /etc/apt/sources.list.d/mongodb.list file
echo 'deb http://downloads-distro.mongodb.org/repo/ubuntu-upstart dist 10gen' | sudo tee /etc/apt/sources.list.d/mongodb.list
# Update your repository
sudo apt-get update
# Install MongoDB
sudo apt-get install mongodb-10gen

Now that that's done, let's configure it to use the EBS volume we setup earlier. The AWS console will say your volume is something like /dev/sdb, but the actual device name in Ubuntu will be something like /dev/xvdb.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# Create the file system on your EBS volume. <device name> is your EBS path
# sudo mkfs -t ext4 <device name>
sudo mkfs -t ext4 /dev/xvdb
# Create a folder wherever you want to mount it
# mkdir <mount point>
mkdir /database
# Add an fstab entry so it gets mounted on system boot
echo '/dev/xvdb /database ext4 defaults,auto,noatime,noexec 0 0' | sudo tee -a /etc/fstab
# Mount it
sudo mount /dev/xvdb /database
# Create directories for the actual data, logs, and journal
cd /database
mkdir data journal log
# Set MongoDB to the owner of these directories
sudo chown -R mongodb:mongodb /database
# And link your journal
ln -s /database/journal /database/data/journal

Now configure MongoDB to use these paths: sudo nano /etc/mongodb.conf

1
2
dbpath = /database/data
logpath = /database/log/mongodb.log

Install Node

See the official wiki for more information about installing Node.js on Ubuntu. Ubuntu's default Node package lags behind the latest stable release. If that's okay, then go ahead:

1
2
3
4
sudo apt-get install nodejs
# Due to a naming conflict, node was renamed to nodejs in apt
# So you'll need to create a symlink to use the command 'node'
ln -s /usr/bin/nodejs /usr/bin/node

But if you want to install the latest version of Node, you're gonna have to do this:

1
2
3
4
5
6
sudo apt-get update
sudo apt-get install python-software-properties python g++ make
sudo apt-add-repository ppa:chris-lea/node.js
sudo apt-get update
sudo apt-get install nodejs
ln -s /usr/bin/nodejs /usr/bin/node

Listening on port 80 requires root privileges. Instead, Node will listen on port 3000, and we'll redirect requests from port 80 to there:

1
sudo iptables -t nat -A PREROUTING -i eth0 -p tcp --dport 80 -j REDIRECT --to-port 3000

Add the command (minus sudo) to your rc.local file to make sure this applies on boot as well: sudo nano /etc/rc.local

Keep your app running forever

For this, we're going to use Forever and Upstart. Forever is used in production at Nodejitsu, and restarts your app if it crashes. Upstart registers your app as a service, starting it on boot and cleanly stopping it on shutdown. The configuration that follows is directly from this awesome guide on exratione.com.

First, install Forever:

1
sudo npm install -g forever

Then, create the following Upstart configuration: sudo nano /etc/init/myapp.conf

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
# My App upstart config /etc/init/myapp.conf
description "Startup script for My App using Forever"

start on startup
stop on shutdown

# So that Upstart reports the pid of the Node.js process started by Forever
# rather than Forever's own pid
expect fork

# Full path to the node binaries
env NODE_BIN_DIR="/usr/bin/node"

# Path for finding global NPM node_modules
env NODE_PATH="/usr/lib/nodejs:/usr/lib/node_modules:/usr/share/javascript"

# Directory containing My App
env APPLICATION_DIRECTORY="/home/ubuntu/myapp"

# Application javascript filename
env APPLICATION_START="server.js"

# Environment to run app as
env NODE_ENV="production"

# Log file
env LOG="/var/log/chirp.log"

script
  PATH=$NODE_BIN_DIR:$PATH

  exec forever --sourceDir $APPLICATION_DIRECTORY --append -l $LOG \
    --minUptime 5000 --spinSleepTime 2000 start $APPLICATION_START
end script

pre-stop script
  PATH=$NODE_BIN_DIR:$PATH
  exec forever stop $APPLICATION_START >> $LOG
end script

Now you can start, restart, and stop your app like this:

1
2
3
sudo service myapp start
sudo service myapp restart
sudo service myapp stop

Ta-Da!

Now your environment is ready to rock'n'roll—you just need to figure out a deployment methodology. Perhaps I'll cover an automated build/deployment process for a MEAN stack app next?

Post a comment if you have any questions, and I'll be happy to do what I can to help.

Comments