使用DigitalOcean部署Node.js App

由于项目的需要,我将学习如何在一台空的Ubuntu机器上,配置Node.js + MongoDB的生产环境,这里是整个的搭建配置流程,使用了DigitalOcean作为VPS,搭建了一个远程的环境,用来部署Node.js App。

搭建并配置VPS

在我们开始一切之前,我们首先需要一个可以访问的服务器。有许多的VPS可以供我们选择,例如AWS,阿里云,DigitalOcean等等,所以不必拘泥于DigitalOcean。不过,为了方便起见,建议根据教程一起,使用DigitalOcean来搭建服务器,因为它真的挺不错的。

如果你还没有DigitalOcean的账户,那么你可以点击下面这个链接,可以免费获得10美元,足够让你用两个月,还挺不错的吧。

在DigitalOcean上创建一个新的Droplet

在创建好账户,并登陆之后,你可以看到如下的界面,点击”Create Droplet”的按钮。

创建Droplet

选择Ubuntu 16.04.3 x64 -> 选择最便宜的每月5美元的型号 -> 选择服务器的所在区域(我选了新加坡,因为感觉离得比较近吧) -> 添加SSH Key

这里,我是用的是Mac OS,所以可以打开terminal,输入如下命令查看是否已经存在SSH key:

1
ls -la ~/.ssh

当返回的列表中,有存在名为 id_rsa.pub 的文件时,说明系统中已经生成了SSH key,只需要复制就可以了,复制的命令为:

1
2
# This copies the key in the clipboard, so you can paste with Command + V.
pbcopy < ~/.ssh/id_rsa.pub

点击”New SSH Key”的按钮,将SSH Key粘贴进去,随便起一个名字,比如Louis’s MBP,然后保存。
在下面,为你的Droplet起一个名字,最后点击”Create”按钮创建它。

选择Droplet种类

创建成功后,页面会跳转到Droplet列表页面,在新的Droplet上会显示它的IP地址,点击复制它。

复制ip地址

打开terminal,输入如下命令,来连接到你的Droplet上:

1
2
# 确保用你的IP地址,替换下面的128.199.175.122
ssh root@128.199.175.122

对服务器进行基础的安全设置

当我们登录到服务器之后,我们需要对服务器做一些基础的配置,来确保它的安全性。

首先,我们要创建一个拥有 sudo 权限的新用户,在terminal中输入如下命令:

1
adduser louis

这个命令会要求我们输入密码,以及一些相关信息,直接回车就可以了。

创建完成后,我们可以使用 id 命令来查看user:

1
2
root@nodejs-deploy:~# id louis
uid=1000(louis) gid=1000(louis) groups=1000(louis)

为了在服务器上运行某些命令,比如说restart services等,我们需要将我们的用户添加到 sudo 用户组里,输入如下命令:

1
usermod -aG sudo louis

执行完成后,再使用 id 命令查看一下louis的状态:

1
2
root@nodejs-deploy:~# id louis
uid=1000(louis) gid=1000(louis) groups=1000(louis),27(sudo)

可以看出,我们的louis用户已经被添加到 sudo 用户组里了。

下面,我们要将我们的SSH key添加给我们的新用户,这样就可以使我们的新用户在登录的时候不需要输入密码了,因为我们打算禁止用户使用密码登录这个服务器来提高它的安全性,所以这一步很重要:

1
2
3
4
5
6
7
8
9
10
11
# switch user to louis
su - louis
# make directory for ssh
mkdir ~/.ssh
# set the permissions to only allow this user into it
chmod 700 ~/.ssh
# create a file to store ssh key
nano ~/.ssh/authorized_keys

这里,在另一个terminal中,输入之前的 pbcopy 命令,来复制ssh key,点击paste之后,输入 ctrl+x,然后退出。要确认是否成功粘贴的话,可以使用如下命令检测:

1
cat ~/.ssh/autohrized_keys

在这之后,修改这个文件的权限并推出:

1
2
3
4
chmod 600 ~/.ssh/authorized_keys
# stop acting as the new user and become root again
exit

接下来,我们需要阻止使用密码的登录行为,因为每个服务器都有一个root用户,而且这是一个常见的攻击目标,它又有着非常强大的权限,所以最好是将它限制起来,确保没有人可以使用它:

1
2
3
4
5
# log out server as root
exit
# log into server as new user
ssh louis@128.199.175.122

现在,我们需要更新SSH的配置文件,来阻止密码登录,同时阻止使用root用户进行登录,输入如下命令来修改配置文件:

1
sudo nano /etc/ssh/sshd_config

这个文件中,需要修改两个地方:

  1. PermitRootLogin Yes 改为 no
  2. PasswordAuthentication Yes 改为 no

TIPS: 可以使用 ctrl+w 来搜索内容进行修改

修改完成后,按下 ctrl+x 以及 y 推出编辑并保存。输入如下命令,重启ssh服务:

1
sudo systemctl reload sshd

这时,新建一个terminal窗口,尝试使用root登录的话,应该会失败。显示Permission denied (publickey)。

接着,我们需要配置一个简单的防火墙,我们将只允许HTTP, HTTPS以及SSH。这将会减少很大一部分的安全隐患,当然这也只是简单的配置,这里不对安全做过多的深入了。

1
2
3
4
5
6
7
8
9
10
11
# enable OpenSSH connections
sudo ufw allow OpenSSH
# enable HTTP traffic
sudo ufw allow http
# enable HTTPS traffic
sudo ufw allow https
# turn the firewall on
sudo ufw enable

在启动完成后,可以使用如下命令查看防火墙的状态:

1
2
3
4
5
6
7
8
9
10
11
louis@nodejs-deploy:~$ sudo ufw status
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
80 ALLOW Anywhere
443 ALLOW Anywhere
OpenSSH (v6) ALLOW Anywhere (v6)
80 (v6) ALLOW Anywhere (v6)
443 (v6) ALLOW Anywhere (v6)

运行Node.js App

到目前为止,我们的服务器已经配置完成了,所以开始准备运行我们的App吧!

安装Git

在Droplet中,其实已经预装了Git,可以使用如下命令查看是否安装:

1
2
louis@nodejs-deploy:~$ git --version
git version 2.7.4

如果没有安装的话,安装起来也很方便:

1
sudo apt-get install git

安装MongoDB

安装MongoDB的步骤可以参照官方文档:https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/

首先,需要导入public key:

1
2
3
4
5
6
7
8
9
10
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 0C49F3730359A14518585931BC711F9BA15703C6
Executing: /tmp/tmp.496APU6jBy/gpg.1.sh --keyserver
hkp://keyserver.ubuntu.com:80
--recv
0C49F3730359A14518585931BC711F9BA15703C6
gpg: requesting key A15703C6 from hkp server keyserver.ubuntu.com
gpg: key A15703C6: public key "MongoDB 3.4 Release Signing Key <packaging@mongodb.com>" imported
gpg: Total number processed: 1
gpg: imported: 1 (RSA: 1)

然后,选择对应的Ubuntu版本,来创建一个list file (Ubuntu 16.04):

1
echo "deb [ arch=amd64,arm64 ] http://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/3.4 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.4.list

更新apt-get:

1
sudo apt-get update

安装最新稳定版本的MongoDB:

1
sudo apt-get install -y mongodb-org

启动MongoDB:

1
sudo service mongod start

要在本机上使用Robomongo连接到远程主机,由于我们关闭了密码验证,所以需要手动将远程主机的27017端口forward到我们的机器上,具体设置如下:

1
$ ssh louis@128.199.175.122 -L 27018:127.0.0.1:27017

在Robomongo的连接面板中,server为127.0.0.1,端口号为27018。这样就可以使用Robomongo访问远程主机上的MongoDB了,还是很实用的。

安装Node.js

跟git相比,安装node.js要比较麻烦一点,因为有许多不同版本的node.js,所以我们要选择我们需要的版本,让 apt-get 来进行正确的更新。执行以下的命令,来获取并执行NodeSource提供的setup script:

1
curl -sL https://deb.nodesource.com/setup_6.x | sudo -E bash -

当上面的执行完成了,我们就可以使用 apt-get 来安装node.js了:

1
sudo apt-get install nodejs

安装完成后,可以查看版本号来检测是否安装成功:

1
2
3
4
louis@nodejs-deploy:~$ node --version
v6.11.4
louis@nodejs-deploy:~$ npm --version
3.10.10

下面我们使用一个小App进行测试:

1
2
3
4
5
6
7
8
cd ~
mkdir apps
cd apps/
git clone https://github.com/bradtraversy/nodekb.git
cd nodekb

安装nodemon:

1
sudo npm install -g nodemon

安装dependencies:

1
npm install

由于这个小程序是运行在3000端口上的,所以我们要将防火墙的3000端口打开,这样才可以访问(后面Nginx可以反向代理)

1
sudo ufw allow 3000

运行App:

1
2
3
4
5
6
louis@nodejs-deploy:~/apps/nodekb$ nodemon
[nodemon] 1.12.1
[nodemon] to restart at any time, enter `rs`
[nodemon] watching: *.*
[nodemon] starting `node app.js`
Server started on port 3000...

打开浏览器,输入地址:http://128.199.175.122:3000/ 就可以访问Node.js程序了。

使用PM2来管理Node.js App

到目前为止,已经可以手动启动并运行Node.js程序了,但是万一我们的服务器重启了,我们就得再重新执行一遍上述的步骤,这很麻烦,所以我们将要使用一个叫做PM2的process manager来管理我们的Node.js Apps。

安装PM2

1
sudo npm install -g pm2

使用PM2启动App

1
2
3
4
5
6
7
8
9
10
11
12
13
cd ~/apps/nodekb
pm2 start app.js
[PM2] Spawning PM2 daemon with pm2_home=/home/louis/.pm2
[PM2] PM2 Successfully daemonized
[PM2] Starting /home/louis/apps/nodekb/app.js in fork_mode (1 instance)
[PM2] Done.
┌──────────┬────┬──────┬───────┬────────┬─────────┬────────┬─────┬───────────┬───────┬──────────┐
│ App name │ id │ mode │ pid │ status │ restart │ uptime │ cpu │ mem │ user │ watching │
├──────────┼────┼──────┼───────┼────────┼─────────┼────────┼─────┼───────────┼───────┼──────────┤
│ app │ 0 │ fork │ 26669 │ online │ 0 │ 0s │ 8% │ 19.7 MB │ louis │ disabled │
└──────────┴────┴──────┴───────┴────────┴─────────┴────────┴─────┴───────────┴───────┴──────────┘

启动应用后,会得到上面的返回结果,说明app已经成功运行了。

设置服务器重启后自动重启应用

1
2
3
4
5
6
7
8
9
# change directory to app folder
louis@nodejs-deploy:~$ cd ~/apps/nodekb
# execute the following command
louis@nodejs-deploy:~/apps/nodekb$ pm2 startup systemd
[PM2] Init System found: systemd
[PM2] To setup the Startup Script, copy/paste the following command:
sudo env PATH=$PATH:/usr/bin /usr/lib/node_modules/pm2/bin/pm2 startup systemd -u louis --hp /home/louis

当执行 pm2 startup systemd 后,系统会返回一串命令要求你执行,这时复制粘贴并执行就可以了。

使用Let’s Encrypt获取免费的SSL Certificate

SSL一直以来因为昂贵和复杂成为其普及的障碍,但是现在,有些非常热心的人创造出了Let’s Encrypt这个好东西,使它变得便宜又简单了。所以我们现在要用它来提高我们域名的安全性。

安装Let’s Encrypt

首先,我们要安装一个Let’s Encrypt的依赖文件,叫做bc:

1
2
3
4
5
# change directory to home
cd ~
# use apt-get to install it
sudo apt-get install bc

完成后,我们使用git,将文件复制进/opt/letsencrypt文件夹

1
sudo git clone https://github.com/letsencrypt/letsencrypt /opt/letsencrypt

将域名指向服务器

由于在生成SSL证书之前,我们需要先将域名指向我们的服务器。

所以这里,我们需要获取一个域名, 这里我使用了腾讯云的域名,目前还在实名认证审核中…😢

域名审核通过,我们继续,在TencentCloud的控制台将域名指向了我们的主机IP地址后,测试也通过了。

1
2
louis@nodejs-deploy:~$ dig +short www.louisnow.cn
128.199.175.122

返回的地址正确!说明绑定已经成功了,这个域名已经可以成功访问了!

生成SSL证书

1
2
3
cd /opt/letsencrypt
./certbot-auto certonly --standalone

在输入上述命令之后,会要求你输入一个email地址,用于接收一些安全通知。然后会要求你输入你的域名,这里我们需要输入两个域名(louisnow.cn,www.louisnow.cn),使用逗号或者空格隔开就可以了。

为SSL证书设置自动续订

处于安全性考虑,Let’s Encrypt会在每90天后超期失效(一般付费的SSL证书都是一年时间),所以我们就需要为这个证书设置自动续订。

Let’s Encrypt也提供了续订的命令:

1
/opt/letsencrypt/certbot-auto renew

但是,如果每过90天,我们就登录服务器检查一遍是否过期,那么将会很蛋疼,所以这里我们需要一个工具为我们来完成自动续订的事情,它叫做 cron 。你可以在terminal中输入以下命令来编辑cron的任务:

1
sudo crontab -e

这里会询问使用哪一种编辑器,由于nano还是比较简单的,所以我们继续使用nano。在文件的最底部输入如下命令:

1
2
3
4
5
# 每周星期一的一点钟运行代码,并将日志存储在指定位置
00 1 * * 1 /opt/letsencrypt/certbot-auto renew >> /var/log/letsencrypt-renewal.log
# 每周星期一的一点半重启nginx,保证使用新生成的SSL证书
30 1 * * 1 /bin/systemctl reload nginx

将域名指向App

在上面的操作中,我们为了方便将3000端口开放出来方便访问调试,其实这种做法是不太安全的,所以我们现在要关闭3000端口,并使用nginx反向代理来完成对于App的访问。参考: UFW防火墙简单设置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
sudo ufw delete allow 3000
Rule deleted
Rule deleted (v6)
sudo ufw status
Status: active
To Action From
-- ------ ----
OpenSSH ALLOW Anywhere
80 ALLOW Anywhere
443 ALLOW Anywhere
OpenSSH (v6) ALLOW Anywhere (v6)
80 (v6) ALLOW Anywhere (v6)
443 (v6) ALLOW Anywhere (v6)

这样,3000端口就关闭了,我们再尝试从外部访问3000端口应该是失败的。

安装Nginx

现在来安装Nginx,这里还是很方便的,使用apt-get就可以了:

1
sudo apt-get install nginx

接下来,我们要确保所有的流量都是在SSL上传输的,我们要将所有非SSL的流量重定向到SSL版本上,如果某人访问了http://www.louisnow.cn那么他会被自动重定向到https://www.louisnow.cn上,所以我们需要修改Nginx的配置文件:

1
sudo nano /etc/nginx/sites-enabled/default

使用 ctrl+k 删除文件中的所有内容,然后将下面的内容copy进去就可以了:

1
2
3
4
5
6
# HTTP — redirect all traffic to HTTPS
server {
listen 80;
listen [::]:80 default_server ipv6only=on;
return 301 https://$host$request_uri;
}

下面还有一步,只需要一行代码,就可以让我们的App更加安全,就是建立一个Diffie-Hellman-Group,虽然我也不是很清楚这里的原理,总之还是加上吧…:

1
sudo openssl dhparam -out /etc/ssl/certs/dhparam.pem 2048

然后,我们需要给SSL创建一个配置文件:

1
sudo nano /etc/nginx/snippets/ssl-params.conf

将下面的内容copy到打开的文件中并保存退出:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
# See https://cipherli.st/ for details on this configuration
ssl_protocols TLSv1 TLSv1.1 TLSv1.2;
ssl_prefer_server_ciphers on;
ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
ssl_ecdh_curve secp384r1; # Requires nginx >= 1.1.0
ssl_session_cache shared:SSL:10m;
ssl_session_tickets off; # Requires nginx >= 1.5.9
ssl_stapling on; # Requires nginx >= 1.3.7
ssl_stapling_verify on; # Requires nginx => 1.3.7
resolver 8.8.8.8 8.8.4.4 valid=300s;
resolver_timeout 5s;
add_header Strict-Transport-Security "max-age=63072000; includeSubDomains; preload";
add_header X-Frame-Options DENY;
add_header X-Content-Type-Options nosniff;
# Add our strong Diffie-Hellman group
ssl_dhparam /etc/ssl/certs/dhparam.pem;

继续配置Nginx文件:

1
sudo nano /etc/nginx/sites-enabled/default

将下面的内容copy到之前的内容后面:

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
# HTTPS — proxy all requests to the Node app
server {
# Enable HTTP/2
listen 443 ssl http2;
listen [::]:443 ssl http2;
server_name www.louisnow.cn;
# Use the Let’s Encrypt certificates
ssl_certificate /etc/letsencrypt/live/www.louisnow.cn/fullchain.pem;
ssl_certificate_key /etc/letsencrypt/live/www.louisnow.cn/privkey.pem;
# Include the SSL configuration from cipherli.st
include snippets/ssl-params.conf;
location / {
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-NginX-Proxy true;
proxy_pass http://localhost:3000/;
proxy_ssl_session_reuse off;
proxy_set_header Host $http_host;
proxy_cache_bypass $http_upgrade;
proxy_redirect off;
}
}

要注意将域名相关的地方进行替换,并且由于我们的App是运行在3000端口上的,所以在proxy_pass后面填上http://localhost:3000/

在启动服务器之前,我们需要先测试一下,看看有没有出问题:

1
sudo nginx -t

这里我就出问题了,由于将配置文件中的域名写成 www.louisnow.cn 导致了错误,这里应该将 www 去掉就可以了,再次测试,通过!

1
2
3
sudo nginx -t
nginx: the configuration file /etc/nginx/nginx.conf syntax is ok
nginx: configuration file /etc/nginx/nginx.conf test is successful

最后,运行nginx即可:

1
sudo systemctl start nginx

访问www.louisnow.cn,当时发现页面不对啊,这里可能因为服务器还没刷新,所以我们回到terminal,重新启动一下nginx:

1
sudo systemctl restart nginx

然后再回到浏览器,按住 shift+command+R 就可以了!

首页

注册界面
注册界面