ASP.NET Core 实战:Linux 小白的 .NET Core 部署之路

2019-09-11 15:16:15 浏览数 (1)

一、前言

   最近一段时间自己主要的学习计划还是按照毕业后设定的计划,自己一步步的搭建一个前后端分离的 ASP.NET Core 项目,目前也还在继续学习 Vue 中,虽然中间断了很长时间,好歹还是坚持下来了,嗯,看了看时间,原本决定的半年完成肯定是完不成了。这两周重新拾起来学习 Vue,文章也在慢慢的更新中,这一篇文章主要是想提前试试水将 ASP.NET Core 部署到 Linux Server 上,原本的打算是把毕业设计就部署到 Linux 上,最终也未能成行,究其原因,还是自己太懒太拖了吧,哈哈哈,拖到最后,毕业设计差点都没写完。

  因为目前自己的前后端分离的项目还没开始写,所以这里采用的还是自己原来写的 .NET Core 项目,这篇文章的主要目的是操作下如何将我们 ASP.NET Core 项目部署到我们的 Linux 服务器上,如果对你有任何的帮助的话,不胜荣幸。当然,如果有不对的地方,欢迎大家提出。

二、准备工作

  1、一台 Linux 操作系统的电脑,嗯,这里采用的是腾讯云学生套餐,服务器版本为 CnetOS 7.5 。

  2、终端软件,这里我使用的是putty,用来帮助我们远程连接到我们的服务器。

  下载链接:https://www.chiark.greenend.org.uk/~sgtatham/putty/latest.html

  3、文件上传软件,这里我使用的是 WinSC,上传文件的,同时,如何你和我一样对于使用命令行编辑文件不习惯的话,用这个还可以编辑下服务器上需要更改的配置文件,逃~~~。

  下载链接:https://winscp.net/eng/download.php

三、Step by Step

1、安装 .NET Core Runtime

因为这里并不需要在 Linux 服务器上进行开发工作,所以只需要安装好 dotnet core runtime 就可以了,如果你需要在 Linux 上进行开发,就需要安装 .NET Core SDK了,当然,如果安装过了 SDK,也就不需要安装 Runtime 了。

  我们打开微软的官方网站,使用 putty 连接到我们的服务器。 官网地址:https://www.microsoft.com/net/download/linux-package-manager/centos/runtime-current

  a)在服务器上注册 Microsoft 秘钥,这里我使用的是 Centos ,这里你需要按照你自己的服务器版本进行选择下载。

代码语言:javascript复制
sudo rpm -Uvh https://packages.microsoft.com/config/rhel/7/packages-microsoft-prod.rpm

  b)更新可供安装的版本,当控制台出现 Complete 时,则代表我们更新完成。

代码语言:javascript复制
sudo yum update

  c)安装 .NET Core,同样的,当控制台出现 Complete 时,则代表我们安装完成。

代码语言:javascript复制
sudo yum install aspnetcore-runtime-2.1  ##这里如果你要在 Linux 上开发,这里就安装 dotnet-sdk-2.1

  这时,如何判断我们的 .NET Core 安装成功了呢?我们可以在控制台上输入下面的命令,如果已经安装成功的话,就可以把我们安装的 dotnet 版本信息显示出来,反之,你就需要重新执行了。

代码语言:javascript复制
dotnet --info

  这里我们可以看到,我们只是安装了 .NET Core Runtime,并没有安装 SDK,我们的 Host 版本是2.1.5。

2、安装 MySQL

  a)添加 MySQL 源

  首先打开 MySQL 的官网,需要根据自己的服务器信息选择合适的源信息,我的服务器操作系统是 CentOS,这里我选择的是 yum 源(yum 源地址:https://dev.mysql.com/downloads/repo/yum/)。

  当我们点击 Download 按钮后发现,要登录。。。如果你和我一样不想又注册一个账户,我们可以获取到下面的下载地址,然后通过 rpm -Ivh 的方式安装。我安装的是最新的 MySQL 8.0 版本,当然,你也可以通过修改版本号下载不同的 MySQL 版本。

代码语言:javascript复制
wget https://dev.mysql.com/get/mysql80-community-release-el7-1.noarch.rpm
rpm -ivh mysql80-community-release-el7-1.noarch.rpm

  b)校验 md5 与官网上的是一致

代码语言:javascript复制
md5sum mysql80-community-release-el7-1.noarch.rpm

  c)安装 MySQL Server

代码语言:javascript复制
sudo yum install mysql-server

  d)启动 MySQL Server

  当我们完成安装之后,就可以启动 MySQL Server 服务了。我们可以使用下面的命令启动 MySQL 守护程序。

代码语言:javascript复制
sudo systemctl start mysqld

  可是上面的命令执行之后,不会有任何的反应,我们可能会疑问我们执行成功了吗?所以为了确保我们执行成功,我们可以使用下面的命令来查看是否启用了 MySQL 服务。此时,如果我们的 MySQL 服务已经启动了,则会输出我们的执行信息。

代码语言:javascript复制
sudo systemctl status mysqld

  当我们启动后可能会疑问,我们并没有设置管理员密码啊?原来,我们在安装的过程中,会自动的为 MySQL root 用户生成一个临时的密码,我们可以通过下面的命令中找到这个默认的密码。

代码语言:javascript复制
sudo grep 'temporary password' /var/log/mysqld.log

  我们可以使用下面的命令来修改我们的 root 密码,系统会提示我们输入默认密码,默认密码输入正确后就可以自己设置新的密码了。

代码语言:javascript复制
sudo mysql_secure_installation

  注意:重新设置的密码至少包含一个大写字母,一个小写字母,一个数字和一个特殊字符的12个字符!!!在整个设置密码的过程中,总共有五步:设置 root 密码;是否禁止 root 账号远程登录;是否禁止匿名账号(anonymous)登录;是否删除测试库;是否确认修改。按照你自己的需求后,设置完成后即可。

  e)设置允许远程登录、

  在上面的设置完成后,我们用自己本地的 Workbench 连接服务器上的数据库,发现无法进行连接,如果你之前使用过远程连接 MySQL Server 你应该会知道,我们需要在 user 表中设置 root 用户允许访问的地址。

代码语言:javascript复制
mysql -h localhost -u root -p ##登录数据库,输入密码后完成登录
use mysql; ##选择 mysql 表
select user,host from user; ##查询当前的用户
update user set host='%' where user='root'; ##允许使用 root 账户进行远程登录
flush privileges; ##刷新设置

  这时,我们就可以远程连接到我们的 MySQL Server 上了。

3、发布部署程序

  本次部署的项目,采用的还是之前的毕业设计的项目(ASP.NET Core 2.0 MVC 项目实战),在这里发布测试的时候遇到了一个问题,因为当时项目采用的 MySQL 版本为5.7,服务器中所安装的 MySQL 版本为8.0,而 Oracle 在最新的 MySQL 中将默认的版本身份验证从 mysql_native_password 改为了 caching_sha2_password,这里在进行数据操作时就会出现问题。如果你和我遇到同样的问题,你需要将 MySQL 官方的 EFCore 组件替换成 Pomelo.EntityFrameworkCore.MySql,嗯,替换,而不是升级,因为升级后又会出现新的问题(小声BB:MySQL 的这个 EFCore 的驱动事不是一般的多)。如果你不想升级的话,可以参考这个做法,链接地址送上:mysql8 :客户端连接caching-sha2-password问题。

  当我们把项目丢到服务器上后,我们进去到放置的路径下,执行 dotnet 命令就可以运行我们的项目了。这里要特别注意,Linux 中对于大小写是区分的,这里输入的路径以及项目的名称都要确保和实际相同。

代码语言:javascript复制
cd /usr/local/wwwroot/psu/ ##注意:最后一定要加上这个  / !!!!!
dotnet PSU.Site.dll

  这里会有个问题,不管你是使用的虚拟机还是云服务器,因为 5000 端口并没有开放给外部访问,所以外部的机器采用 ip:port 的方式,是无法访问到的,所以我们接下来需要安装反向代理的服务器来达到访问的目的。

  在部署 .NET Core 项目的时候,我们应该保持我们的程序的 .NET Core 版本与服务器上的环境版本保持一致,这样才可以避免因为环境的因素而导致的某些问题,所以这里我部署 .NET Core 2.0 版本的程序其实不是很好的选择。

4、安装 Nginx 服务器

  在 Windows 服务器上,如果我们要部署 .NET 项目,肯定会选择部署到 IIS 中,同样的,虽然 .NET Core 可以实现自托管,内置的 Kestrel 也非常适合从 ASP.NET Core 提供动态内容。 但是,Kestrel 的 Web 服务功能不像专门的服务器(如 IIS、Apache 或 Nginx)那样功能丰富。 而反向代理服务器可以从 HTTP 服务器卸载服务静态内容、缓存请求、压缩请求和 SSL 终端等工作。 反向代理服务器可能驻留在专用计算机上,也可能与 HTTP 服务器一起部署可是为了能使用更多的功能,所以这里我们还是会配合一个反向代理服务器进行使用,在这里,我采用的是 Nginx。

  a)安装 Nginx

  从官网上下载 nginx 包并解压,这里采用的是使用源码编译安装的形式。

代码语言:javascript复制
wget -c http://nginx.org/download/nginx-1.9.9.tar.gz
tar -zxvf nginx-1.9.9.tar.gz

  解压完成后我们就可以进入到 nginx 目录,在这里我们创建默认的配置文件。在我们创建配置文件之前,我们需要安装 gcc 环境。

代码语言:javascript复制
cd nginx-1.9.9
yum install gcc gcc-c  
./configure

  上面的步骤完成后,我们需要添加几个插件去完善 Nginx 的功能。

  Nginx 的 http 模块使用 pcre 来解析正则表达式,PCRE(Perl Compatible Regular Expressions) 是一个Perl库,包括 perl 兼容的正则表达式库。

代码语言:javascript复制
yum install -y pcre pcre-devel

  zlib 库提供了很多种压缩和解压缩的方式,Nginx 使用 zlib 对 http 包的内容进行 gzip ,所以我们也需要在服务器上安装 zlib 库。

代码语言:javascript复制
yum install -y zlib zlib-devel

  Nginx 不仅支持 http 协议,还支持 https 协议,而 OpenSSL 是一个强大的安全套接字层密码库,囊括主要的密码算法、常用的密钥和证书封装管理功能及 SSL 协议,并提供丰富的应用程序供测试或其它目的使用,所以我们也会在 Nginx 上面添加。

代码语言:javascript复制
yum install -y openssl openssl-devel

  至此,我们对于 Nginx 插件的安装就完成了,现在我们就可以重新执行配置命令,再编译安装了。

代码语言:javascript复制
./configure
make install

  编译安装完成后,Nginx 就安装到了我们的服务器上,可是安装到哪里去了呢?这里我们可以使用下面的命令去找到我们安装的 Nginx。知道了安装路径后,我们就可以进入 Nginx 的目录中执行进一步的操作。至此 Nginx 的安装就完成了(这里是我的路径地址,你的可能与我不同,以你的实际为准)。

代码语言:javascript复制
whereis nginx
cd /usr/local/nginx/sbin/

  安装完成后,我们一般会将 Nginx 设置为开机自启动以及自动重启,从而预防特殊情况导致的网站挂了。我们先创建一个软连接用来指向 Nginx 的安装目录。

代码语言:javascript复制
ln -s /usr/local/nginx/sbin/nginx /usr/bin/nginx

  现在创建一个自启动的脚本。

代码语言:javascript复制
vim /etc/init.d/nginx

  在脚本中,输入下面的内容。

代码语言:javascript复制
#!/bin/sh
#
# nginx - this script starts and stops the nginx daemon
#
# chkconfig:   - 85 15
# description:  NGINX is an HTTP(S) server, HTTP(S) reverse 
#               proxy and IMAP/POP3 proxy server
# processname: nginx
# config:      /usr/local/nginx/conf/nginx.conf
# config:      /etc/sysconfig/nginx
# pidfile:     /usr/local/nginx/logs/nginx.pid
# Source function library.
. /etc/rc.d/init.d/functions
# Source networking configuration.
. /etc/sysconfig/network
# Check that networking is up.
[ "$NETWORKING" = "no" ] && exit 0
nginx="/usr/local/nginx/sbin/nginx"
prog=$(basename $nginx)
NGINX_CONF_FILE="/usr/local/nginx/conf/nginx.conf"
[ -f /etc/sysconfig/nginx ] && . /etc/sysconfig/nginx
lockfile=/var/lock/subsys/nginx
make_dirs() {
   # make required directories
   user=`$nginx -V 2>&1 | grep "configure arguments:" | sed 's/[^*]*--user=([^ ]*).*/1/g' -`
   if [ -z "`grep $user /etc/passwd`" ]; then
       useradd -M -s /bin/nologin $user
   fi
   options=`$nginx -V 2>&1 | grep 'configure arguments:'`
   for opt in $options; do
       if [ `echo $opt | grep '.*-temp-path'` ]; then
           value=`echo $opt | cut -d "=" -f 2`
           if [ ! -d "$value" ]; then
               # echo "creating" $value
               mkdir -p $value && chown -R $user $value
           fi
       fi
   done
}
start() {
    [ -x $nginx ] || exit 5
    [ -f $NGINX_CONF_FILE ] || exit 6
    make_dirs
    echo -n $"Starting $prog: "
    daemon $nginx -c $NGINX_CONF_FILE
    retval=$?
    echo
    [ $retval -eq 0 ] && touch $lockfile
    return $retval
}
stop() {
    echo -n $"Stopping $prog: "
    killproc $prog -QUIT
    retval=$?
    echo
    [ $retval -eq 0 ] && rm -f $lockfile
    return $retval
}
restart() {
    configtest || return $?
    stop
    sleep 1
    start
}
reload() {
    configtest || return $?
    echo -n $"Reloading $prog: "
    killproc $nginx -HUP
    RETVAL=$?
    echo
}
force_reload() {
    restart
}
configtest() {
  $nginx -t -c $NGINX_CONF_FILE
}
rh_status() {
    status $prog
}
rh_status_q() {
    rh_status >/dev/null 2>&1
}
case "$1" in
    start)
        rh_status_q && exit 0
        $1
        ;;
    stop)
        rh_status_q || exit 0
        $1
        ;;
    restart|configtest)
        $1
        ;;
    reload)
        rh_status_q || exit 7
        $1
        ;;
    force-reload)
        force_reload
        ;;
    status)
        rh_status
        ;;
    condrestart|try-restart)
        rh_status_q || exit 0
            ;;
    *)
        echo $"Usage: $0 {start|stop|status|restart|condrestart|try-restart|reload|force-reload|configtest}"
        exit 2
esac

  现在我们将自启动的脚本赋予脚本可执行权限,并将 Nginx 服务加入 chkconfig 管理列表中。

代码语言:javascript复制
chmod a x /etc/init.d/nginx
 
chkconfig --add /etc/init.d/nginx
chkconfig nginx on

  当上面的设置都完成后,启动我们的 Nginx 守护服务就可以了。

代码语言:javascript复制
systemctl start nginx

  同样的,我们也可以使用下面的命令来查看 Nginx 服务的状态。

代码语言:javascript复制
systemctl status nginx

  一些常用的命令(需要你处于 Nginx 的安装目录下时执行,例如,这里我的 Nginx 安装目录为 /usr/local/nginx/sbin/,如果在不在安装目录下需要使用全路径)

  启动 Nginx 服务

代码语言:javascript复制
./nginx

  停止 Nginx 服务

代码语言:javascript复制
./nginx -s stop
./nginx -s quit

  停止 Nginx 服务有两种方式,第一种:-s stop:先查出 nginx 的进程 id ,再使用 kill 命令强制杀掉进程;第二种:-s quit:等待 Nginx 的进程处理完成后再进行停止。

  重新加载 Nginx 配置

代码语言:javascript复制
./nginx -s reload
5、配置守护程序以及自启动

  在上面我们已经使用 dotnet 命令将我们的项目在服务器上运行了,而我们目前通过 ip:port 的形式没有办法进行访问,这时我们安装的 Nginx 服务器就可以为我们提供帮助了。

  在安装 Nginx 的时候我们创建了一个配置文件,现在我们就可以通过编辑这个配置文件,将我们的项目使用 Nginx 进行代理。首先我们进入 Nginx 的配置文件所在的路径,打开这个配置文件。

代码语言:javascript复制
cd /usr/local/nginx/conf/
vim nginx.conf

  打开 conf 文件后,我们找到 Server 节点,会看到以下的配置项。

代码语言:javascript复制
server {
  listen        80;
  server_name   localhost;
  location / {
        root   html;
        index  index.html index.htm;
  }
}

  当使用 Nginx 时,会根据 server_name 匹配服务器,例如当我们运行 Nginx 成功后,通过浏览器浏览本机的 ip 时,默认会显示 Nginx 的默认页面。而当没有匹配的 server_name 时,Nginx 则会使用默认服务器。 如果没有定义默认服务器,则配置文件中的第一台服务器则成为默认服务器。

  这里进行修改配置信息,将80端口的请求转发到我们使用 Kestrel 监听的5000端口上的应用上。

代码语言:javascript复制
server {
  listen        80;
  server_name   localhost;
  location / {
    # Kestrel
    proxy_pass            http://localhost:5000;
    proxy_http_version    1.1;
    proxy_set_header      Upgrade $http_upgrade;
    proxy_set_header      Host $http_host;
    proxy_cache_bypass    $http_upgrade;
    }
}

  我们可以使用命令去验证下修改的配置格式是否正确,配置正确后就可以执行 reload 命令使配置生效。

代码语言:javascript复制
./nginx -t ##测试格式是否正确
./nginx -s reload ##重新加载配置

  当我们完成上面的步骤后,通过浏览器打开我们的页面,毫无意外的 Nginx 的错误页面出现在了我们的面前。仔细梳理下我们的流程,用户通过浏览器请求 ip,Nginx 将默认的 80 端口的请求反向代理转接到我们应用程序的 5000 端口上,而现在我们并没有使用 dotnet 命令来运行我们的程序,服务器上的 5000 端口也就没有程序在监听。因此当我们在使用 Nginx 进行反向代理我们的 .NET Core 程序时,我们同样需要使用 dotnet 命令将我们的程序运行起来。

  现在,将我们的程序重新使用 dotnet 命令运行起来,打开我们的浏览器访问就会发现我们的网站已经部署成功了。而且浏览器的插件也已经识别出我们使用的 Web 服务器为 Nginx 的 1.9.9 版本。

  所以这里会引出一个问题,万一 dotnet 进程意外挂了,整个网站不就彻底挂了吗,难道还要我们手动连接到服务器再次创建?所以我们需要能够让 dotnet 进程能够自动重启,从而避免这种情况。

  微软官方则建议我们使用 supervisor 守护程序的方式实现我们守护我们的 .NET Core 程序,确保应用服务即使闪退也会自动重启。同时,Supervisor 也包含一个 web 管理页面,从而方面我们的管理。

  在 linux 或者 unix 操作系统中,守护进程(Daemon)是一种运行在后台的特殊进程,它独立于控制终端并且周期性的执行某种任务或等待处理某些发生的事件。由于在linux中,每个系统与用户进行交流的界面称为终端,每一个从此终端开始运行的进程都会依附于这个终端,这个终端被称为这些进程的控制终端,当控制终端被关闭的时候,相应的进程都会自动关闭。但是守护进程却能突破这种限制,它脱离于终端并且在后台运行,并且它脱离终端的目的是为了避免进程在运行的过程中的信息在任何终端中显示并且进程也不会被任何终端所产生的终端信息所打断。它从被执行的时候开始运转,直到整个系统关闭才退出。

  输入下面的命令安装 Supervisor 程序。

代码语言:javascript复制
sudo yum install supervisor

  安装完成后我们就可以配置我们的 Supervisor 程序。

  首先,我们在 etc 目录下创建存储我们配置文件的目录。

代码语言:javascript复制
mkdir -m 700 -p /etc/supervisor

  目录创建完成后,我们创建 supervisor 的配置文件,这里采用的是将默认的配置文件复制一份到我们的目录下面。

代码语言:javascript复制
echo_supervisord_conf > /etc/supervisor/supervisord.conf

  在 /etc/supervisor 目录下我们创建一个存放我们 dotnet core 进程文件的存放目录 conf.d。

代码语言:javascript复制
mkdir -m 700 /etc/supervisor/conf.d

  修改我们的配置文件,在配置文件的最后,将路径指向我们创建的 conf.d 文件夹。注意注意,这里一定要把前面的 ; 去掉,否则的话这个 include 节点还是被注释无法被应用的!!!

代码语言:javascript复制
[include]
files=/etc/supervisor/conf.d/*.conf

  现在我们就创建一个 PSU.conf 配置文件来管理我们这个项目的 dotnet 进程。嗯,这里我还是采用 WinSCP 的方式进行编辑,同时,我们需要将注释的信息删除。

代码语言:javascript复制
cd /etc/supervisor/conf.d/
touch PSU.conf
代码语言:javascript复制
[program:PSU.Site]
command=dotnet PSU.Site.dll  #要执行的命令
directory=/usr/local/wwwroot/psu/ #命令执行的目录
environment=ASPNETCORE__ENVIRONMENT=Production #环境变量
user=root  #进程执行的用户身份
stopsignal=INT
autostart=true #是否自动启动
autorestart=true #是否自动重启
startsecs=3 #自动重启间隔
stderr_logfile=/usr/local/wwwroot/logs/psu.err.log #标准错误日志
stdout_logfile=/usr/local/wwwroot/logs/psu.log #标准输出日志

  这里我们指明的日志输出的文件,需要我们事先创建好。

  配置完成后我们就可以创建 Supervisor 的自启动服务。

代码语言:javascript复制
vim /etc/systemd/system/supervisor.service

  编辑我们的自启动脚本。

代码语言:javascript复制
[Unit]
Description=supervisor

[Service]
Type=forking
ExecStart=/usr/bin/supervisord -c /etc/supervisor/supervisord.conf
ExecStop=/usr/bin/supervisorctl shutdown
ExecReload=/usr/bin/supervisorctl reload
KillMode=process
Restart=on-failure
RestartSec=42s

[Install]
WantedBy=multi-user.target

  重新载入我们的设置。

代码语言:javascript复制
systemctl daemon-reload

  设置 supervisor.service 服务开机启动。

代码语言:javascript复制
systemctl enable supervisor.service

  启动我们的服务

代码语言:javascript复制
systemctl start supervisor.service

  现在我们就使用命令查看我们的程序是否运行,最后的 PSU.Site 则是你设置的配置文件里的 program 名称。

代码语言:javascript复制
[root@VM_0_3_centos ~]# ps -ef | grep PSU.Site
root      1382   545  4 19:45 ?        00:00:05 dotnet PSU.Site.dll
root      1648  1606  0 19:47 pts/0    00:00:00 grep --color=auto PSU.Site

  如果这里你无法看到两个进程的话,则说明你的程序没有启动成功,你可以去之前设置的程序的错误日志文件处查看因为什么原因导致的程序无法启动。同时,当你对配置文件做了任何的改变后,你都需要将 Supervisor 进行重启。

四、总结

   这次的文章应该是目前写过时间最长也是字数最多的一篇了,就像标题写的一样, Linux 小白,整个过程持续了很多天,中间在重装系统的过程中碰巧还遇到了 aspnetcore 的一个 bug(Missing package dotnet-runtime 2.1.6 for CentOS),看到 bug 关闭后,周六又弄了四五个小时按照步骤一步步走下来才完成了整个的部署。网上有很多将 .NET Core 程序部署到 Linux 服务器的文章,可是,看再多遍,当你尝试的时候,还是会发生很多的问题,如果你有将 .NET Core 程序部署到 Linux 服务器上的计划时,希望你可以实际尝试尝试,毕竟,踩的坑多了,稍微也能避开一点坑了,哈哈哈。

五、参考

  1、如何在CentOS 7上安装MySQL

  2、CentOS 7 下 Nginx安装以及配置

  3、CentOS 7 源码编译安装 Nginx

  4、ASP.NET Core Linux下为 dotnet 创建守护进程(必备知识)

  5、Centos 7 .Net core后台守护进程Supervisor配置

0 人点赞