从 0 开始部署你的 Node 应用(Ubantu、docker、nginx)

2,166 阅读10分钟

缘起

工作以来,我对服务器部署、dockernginx 这些东西只有一些模糊的印象(面试背的答案)。 但对一个程序员来说,这些又是必会的东西。所以买了服务器,部署了一个简单的 node 项目。 详细地记录下来,希望可以帮助到像我一样服务器小白的同学。

以后和面试官、后端同学聊服务器,就不会一脸懵逼了 =.=

购买服务器

我找到的便宜服务器是 阿里云云服务器ECS

选择按量付费 - 共享型, 个人项目选最低配就行啦。不使用的时候关机,100 元就可以用很久。

建议大家选择 Ubantu 18,因为这个系统是网上教程最多最全的。

服务器配置

为应用单独创建 user

root 用户权限最高,如果误操作会影响整个系统。

所以第一步,就是为自己的应用创建单独的用户。

  1. 用密码登录服务器
ssh root@121.36.50.175
  1. 添加新用户
adduser blog
  1. 切换为 blog
su - blog

blog 用户的根目录是 /home/blog,root 是 /

使用 ssh 登录服务器

我们每次登录服务器都需要输入密码,太麻烦了。

我们给服务器上传 ssh pub key,使用 ssh 的方式登录服务器,就不用输入密码啦。

上传 ssh pub key

如果之前生成过 ssh pub key(比如登录 github),直接上传即可。

ssh-copy-id root@ip
ssh-copy-id blog@ip

配置环境

在命令行中输入 ssh root@你的 ip,用 root 登录服务器,安装软件 dockernode

docker

按照 docker 官网说明依次运行命令即可:

sudo apt-get update
sudo apt-get install \
    apt-transport-https \
    ca-certificates \
    curl \
    gnupg-agent \
    software-properties-common
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -
sudo apt-key fingerprint 0EBFCD88
sudo add-apt-repository \
   "deb [arch=amd64] https://download.docker.com/linux/ubuntu \
   $(lsb_release -cs) \
   stable"
sudo apt-get update
sudo apt-get install docker-ce docker-ce-cli containerd.io

这个时候 blog 用户还不能使用 docker, 我们需要给 blog 用户添加权限。

usermod -a -G docker blog
# 切换到 blog 用户
su - blog

node

node 的安装步骤也一样,一步一步运行即可。 最后顺便安装下 yarn

# Using Ubuntu
curl -sL https://deb.nodesource.com/setup_12.x | sudo -E bash -
sudo apt-get install -y nodejs

sudo apt-get install gcc g++ make

curl -sL https://dl.yarnpkg.com/debian/pubkey.gpg | sudo apt-key add -
echo "deb https://dl.yarnpkg.com/debian/ stable main" | sudo tee /etc/apt/sources.list.d/yarn.list
sudo apt-get update && sudo apt-get install yarn

创建数据库 postgres

这是我的项目专用的数据库,放在 ~/blog-data 目录下

# 创建目录
mkdir blog-data
# 回到代码
cd app/nextjs-blog
# 创建博客项目数据库 
docker run --network=host  -v /home/blog/blog-data:/var/lib/postgresql/data -p 5432:5432 -e POSTGRES_USER=blog -e POSTGRES_HOST_AUTH_METHOD=trust -d postgres:12.2

docker 化我们的 node 项目

下面回到我们的代码,将我们的应用 docker 化,并上传到 docker hub 上。

参考 node 官方文档

Dockerfile

在根目录创建 Dockerfile

# 使用 node v12
FROM node:12
# 程序目录
WORKDIR /usr/src/app
# 使用 yarn 安装依赖
COPY package.json ./
COPY yarn.lock ./

RUN yarn install
# 根目录所有代码拷贝到 /usr/src/app
COPY . .
# 保留端口 3000
EXPOSE 3000
# 启动代码
CMD ["yarn""start"]

.dockerignore

在根目录创建 .dockerignore,它和 .gitignore 用法一样,忽略不需要上传的文件。

node_modules
*.log

构建自己的镜像

docker build -t Marica/node-web-app .

上传镜像

  1. 首先在 Docker Hub 上注册一个账号: 官网地址:https://hub.docker.com/
  2. 控制台使用 docker login 登录账号
  3. 上传
docker push <hub-user>/<repo-name>
docker push Marica/next-blog

在服务器上拉取镜像

接下来回到服务器,我们直接在服务器上拉取 docker 镜像就好啦,非常方便。

# 因为我的项目,docker 容器中的 node 项目需要和 psql 通信,
# 所以添加 --network=host(后面会详细说明)
# --network=host 会导致端口映射失效,端口直接就是阿里云机器的端口,但这种模式比较容易理解
docker run --network=host -p 3000:3000 Marica/next-blog

添加阿里云安全策略

最后,添加阿里云安全策略,开放 80 端口。

添加 nginx

docker 启动命令 & nginx 配置

使用 docker 启动 nginx: (后面会解释为什么使用 --network=host

docker run --name nginx1 --network=host \
# 因为 nginx 无法访问到外部文件,所以需要 -v 将外部文件映射到容器内部
# 容器外 nginx.conf 映射到容器内 default.conf
-v /home/blog/nginx.conf:/etc/nginx/conf.d/default.conf \
-v /home/blog/app/nextjs-blog/.next/static/:/usr/share/nginx/html/_next/static/ \
-d nginx:1.19.1

app/nextjs-blog 目录下创建 nginx.conf

server {
  listen       80; # IPV 4
  listen  [::]:80; # IPV 6
  server_name  localhost;
# 配置 gzip
  gzip on;
  gzip_disable "msie6";

  gzip_comp_level 6;
  gzip_min_length 1100;
  gzip_buffers 16 8k;
  gzip_proxied any;
  gzip_types
    text/plain
    text/css
    text/js
    text/xml
    text/javascript
    application/javascript
    application/x-javascript
    application/json
    application/xml
    application/rss+xml
    image/svg+xml/javascript;
  # 静态文件代理
  location ~ ^/nextjs-blog/_next/static/  {
    root    /usr/share/nginx/html/;
    expires 30d;
  }
  # 把 80 反向代理到 3000
  location / {
    proxy_pass   http://0.0.0.0:3000;
  }
}

我们用 nginx 做这三件事:

1. 动静分离:

什么是「动态资源」?什么是「静态资源」?

  • 动态资源指「接口」等后端资源,静态资源是 HTML,JavaScript,CSS,img等前端文件。 怎么实现「动静分离」?
  • 如果是「静态资源」的请求,就直接到 nginx 配置的静态资源目录下面获取资源。
  • 如果是「动态资源」的请求,nginx 利用反向代理的原理,把请求转发给后台应用去处理,从而实现动静分离。

在使用前后端分离之后,可以提升静态资源的访问速度。 因为 nginx 直接响应文件,而 node 还需要先读取静态文件再响应。

server {
   # ...
    location ~ ^/nextjs-blog/_next/static/  {
            root    /usr/share/nginx/html/;
            expires 30d;
    }
    # ...
}

2. Gzip 压缩

Nginx 开启 Gzip 压缩功能, 对网站的 css、js、xml、html 等文件压缩,提高访问速度。

在用户接收到返回内容之前对其进行处理,以压缩后的数据展现给客户。

可以节约大量的出口带宽,提高传输效率。

server {
  # ...
  gzip on; # 开启gzip模块
  gzip_disable "msie6"# 指定哪些不需要 gzip 压缩的浏览器( IE5.5 和 IE6 SP1 使用 msie6 参数来禁止gzip压缩 )

  gzip_comp_level 6; # 设置 gzip 压缩等级,等级越低压缩速度越快文件压缩比越小,反之速度越慢文件压缩比越大;等级1-9,最小的压缩最快 但是消耗cpu
  gzip_min_length 1100; # 设置允许压缩的页面最小字节,当返回内容大于此值时才会使用gzip进行压缩, 当值为0时,所有页面都进行压缩。建议大于1k
  gzip_buffers 16 8k; #设置gzip申请内存的大小
  gzip_proxied any; 
  # nginx做为反向代理时启用, 根据某些请求和应答来决定是否在对代理请求的应答启用gzip压缩
  # off(关闭所有代理结果的数据的压缩)
  # expired(启用压缩,如果header头中包括"Expires"头信息)
  # no-cache(启用压缩,header头中包含"Cache-Control:no-cache"),
  # no-store(启用压缩,header头中包含"Cache-Control:no-store"),
  # private(启用压缩,header头中包含"Cache-Control:private"),
  # no_last_modefied(启用压缩,header头中不包含 "Last-Modified"),
  # no_etag(启用压缩,如果header头中不包含"Etag"头信息),
  # auth(启用压缩,如果header头中包含"Authorization"头信息)
  # any - 无条件启用压缩

  gzip_types  # 压缩类型
    text/plain
    text/css
    text/js
    text/xml
    text/javascript
    application/javascript
    application/x-javascript
    application/json
    application/xml
    application/rss+xml
    image/svg+xml/javascript;
    # ...
}

3. 反向代理

反向代理是指先以代理服务器来接受客户端上的连接请求,然后将请求转发给内部网络上的服务器。

并将从服务器上得到的结果返回给客户端,此时代理服务器对外就表现为一个服务器。

可以使用多个 node 容器,实现负载均衡。

# 把 80 反向代理到 3000
  location / {
    proxy_pass   http://0.0.0.0:3000;
  }

--network=host

最后,回到之前遗留的问题,为什么使用 --network=host参考这位大佬的文章

不使用 host

这种情况下 nginx.conf 中的 localhost 有问题。 由于 nginx 是运行在 docker 容器中的,这个localhost 是容器的 localhost,而不是宿主机的localhost。

host vs brige

Docker 容器运行的时候有 host、bridge、none 三种网络可供配置。

默认是 bridge,即桥接网络,以桥接模式连接到宿主机:

host是宿主网络,即与宿主机共用网络; none则表示无网络,容器将无法联网。

使用 host

当容器使用 host 网络时,容器与宿主共用网络,这样就能在容器中访问宿主机网络, 那么容器的 localhost 就是宿主机的 localhost。

在 docker 命令中使用 --network host 来为容器配置host网络:

docker run --network=host Marica/next-blog
docker run --name nginx1 --network=host

上面的命令中,没有必要像前面一样使用 -p 80:80 映射端口,是因为本身与宿主机共用了网络, 容器中暴露端口等同于宿主机暴露端口。

使用 host 网络不需要修改 nginx.conf,仍然可以使用 localhost,因而通用性比上一种方法好。

(但是,由于 host 网络没有 bridge 网络的隔离性好,使用 host 网络安全性不如 bridge 高。)

一键部署

最后,根据自己的项目添加部署脚本 deploy.sh

我的脚本是: (我选择直接在服务器上 docker 化 node 项目,省去了上传 docker hub 这一步,因为上传还挺废时间的)

#!/usr/bin/env bash
docker start 54f &&
cd /home/blog/app/nextjs-blog/ &&
git pull &&
yarn install --production=false &&
yarn build &&
yarn compile &&
yarn m:run &&
git reset --hard HEAD &&
docker build -t sunnyla/node-web-app . &&
docker kill app &&
docker rm app &&
docker run --name app --network=host  -p 3000:3000 -d sunnyla/node-web-app &&
echo "ok!"

使用 SSH 在远程服务器上运行本地 Shell 脚本:ssh blog@dev1 'bash -s' < deploy.sh

就可以实现一键部署啦!

作者水平有限,文章中如果有错误恳请各位大佬指出,感谢~!