博客构建之路-部署

创建时间:2017/2/11 下午11:20:22
编辑时间:2017/3/5 下午1:39:43
作者: huww98@163.com (huww98@163.com)
分类:博客构建之路

最近在搞部署环节,我就先写这一部分吧。我打算介绍一下我把我的网站部署到腾讯云的Ubuntu系统的云服务器上的过程,记录一下遇到的一些坑。

手动部署

微软官网有专门针对这个情景的文档:Publish to a Linux Production Environment,写得很不错,但是就给我这样的新手带来了很多需要研究的地方。

安装软件包

根据官网的.NET Core installation guide安装.NET Core。(如果你使用独立部署,这一步可以跳过,因为.NET Core运行时已经包含在你的程序里了。

另外,还需要安装Nginx作为反向代理,以及安装MySQL数据库。在远程服务器上执行以下指令:

sudo apt-get install mysql-server nginx

拷贝文件

首先运行以下指令以打包要上传到服务器的文件:

dotnet publish --configuration Release

那么,如何快速高效地把文件复制到远程服务器上呢?我试过几种方案:

  • 最简单的是使用Windows下的SCP图形界面应用程序进行复制。这种方法速度还可以,但是在处理大量小文件的时候速度不是很理想
  • 在远处服务器上配置FTP服务,使用Visual Studio的发布功能直接发布到FTP服务器上。这种方法在处理小文件时相当的慢,不知道是不是我哪里没弄好。
  • 编写脚本,先用tar指令压缩,再用scp上传,再解压。比第一种方法快一些。

直到我找到了rsync这个程序,它可以高效地处理小文件。在我的电脑上上传速度可以达到1MB/s。而且,它是增量传输的,只会传输更改过的内容,如果更改不多的话,2秒左右就可以完成,速度和其他方法比不是一个数量级的!在本地执行以下指令即可上传当前目录的文件:

rsync -zrv ./ ubuntu@huww98.cn:/var/aspnetcore/myblog/

这里,我打开了压缩传输、递归子目录和详细输出选项。

要注意权限问题,你要同时有目录的w和x权限才能往这个目录写入文件。在我看来,目录的w和x权限似乎没有什么分别,不知是否是我哪里弄错了。

设置服务器数据库连接

服务器上的数据库连接一般都是和开发环境中不一样的,所以有必要单独配置。

在远程服务器部署程序的文件夹中创建一个新文件appsettings.Production.json

{
  "ConnectionStrings": {
    "DefaultConnection": "server=localhost;userid=root;pwd=yourpassword;port=3306;database=myblog;sslmode=none;"
  }
}

其中DefaultConnection应该和你的appsettings.json中连接字符串的名字一样,yourpassword应该替换成你在安装MySQL时设置的密码。

检查startup.cs中的下面这行,该行用于加载上面的配置文件。

.AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true);

配置MySQL

MySQL默认的配置不支持存储中文,需要把默认字符集设置为UTF8

这MySQL的配置文件还是挺复杂的,全局的配置在/etc/mysql/my.cnf,它又包含了/etc/mysql/mysql.conf.d/mysqld.cnf,这里有有关MySQL服务器的配置,我们就修改这个文件吧。在[mysqld]组下面增加:

character-set-server    = utf8mb4
collation-server        = utf8mb4_unicode_ci
skip-character-set-client-handshake

为啥是utf8mb4而不是utf8呢?为啥还要有collation-server呢,请参见:What's the difference between utf8_general_ci and utf8_unicode_ci。最后一行是为了忽略客户端的字符集设置,保证始终以utf8传输。

完成后重启MySQL服务:

sudo service mysql restart

完成之后,就可以试试程序可否正常运行了。对于我的程序,运行:

dotnet MyBlog.dll

再打开另一个控制台,执行:

w3m http://localhost:5000/

就可以在这个简陋的界面中浏览一下网页啦!

配置Nginx

这个应用已经可以在服务器上跑起来了,下面就是如何让外网的用户能够访问的问题了。这里使用Nginx做反向代理,至于原因,上述微软的文档里已经说的很清楚了。

此处与微软的文档略有不同,我在/etc/nginx/sites-available/中新建文件myblog

server {
    listen       80;
    server_name  139.199.6.12;

    root /var/aspnetcore/myblog/wwwroot;

    location ~* ^/((lib|UploadedImages|css|js|img)/|favicon.ico) {
        try_files $uri =404;
    }

    location / {
        proxy_pass        http://localhost:5000;
        proxy_set_header  X-Forwarded-Host    $host;
        proxy_set_header  X-Forwarded-For     $proxy_add_x_forwarded_for;
        proxy_set_header  X-Forwarded-Proto   $scheme;
    }
}

有关HTTP版本的部分我还没有弄懂,所以在此没有写入配置文件。我的网站还没有备案,所以我只希望通过IP地址访问,故加入server_name指令。另外,我让Nginx直接发送静态文件,而不要把请求转交给后端,也多增加了一些proxy_set_header指令,这些header将交由应用中的Microsoft.AspNetCore.HttpOverrides包处理,代码如下,在startup.cs中:(此处也与文档略有不同)

app.UseForwardedHeaders(new ForwardedHeadersOptions
{
    ForwardedHeaders = ForwardedHeaders.All
});

运行sudo nginx -s reload来让Nginx重新加载配置。快快打开浏览器看看我们的杰作吧!

监控应用

别急,至此还没有结束,我们还需要监控我们的应用,以实现开机自动启动,异常时自动重启等。安照微软的文档,使用systemd。创建service文件:/etc/systemd/system/kestrel-myblog.service

[Unit]
    Description=Hu Weiwen's Blog, ASP.NET Core MVC Application

    [Service]
    WorkingDirectory=/var/aspnetcore/myblog
    ExecStart=/usr/bin/dotnet /var/aspnetcore/myblog/MyBlog.dll
    Restart=always
    RestartSec=10
    SyslogIdentifier=myblog
    User=www-data
    Environment=ASPNETCORE_ENVIRONMENT=Production

    [Install]
    WantedBy=multi-user.target

文档中的配置文件那行注释似乎需要删掉。另外,最后一行不是很懂是啥意思,systemd还有待我进一步学习。OK,接下来就是依葫芦画瓢了,执行:

sudo systemctl enable kestrel-myblog.service
sudo systemctl start kestrel-myblog.service
systemctl status kestrel-myblog.service

要加sudo,否则要输密码。最后一行查看状态别打太快了,给它点时间启动。看到Application started. Press Ctrl+C to shut down.就大功告成啦!

这里却出现了一个奇怪的问题,就是我一旦重启了这个服务,博客的所有用户都需要重新登录了,这可不好。看看日志,我发现了两条警告:

warn: Microsoft.Extensions.DependencyInjection.DataProtectionServices[59]
Neither user profile nor HKLM registry available. Using an ephemeral key repository. Protected data will be unavailable when application exits.
warn: Microsoft.AspNetCore.DataProtection.Repositories.EphemeralXmlRepository[50]
Using an in-memory repository. Keys will not be persisted to storage.

看上去与这个问题有关。

我回头看了看之前直接在ssh连接中用dotnet指令启动应用时的输出,发现它会把一个密钥存放在/home/ubuntu/目录下,可是www-data用户默认并没有home目录,这就是问题的原因了。我的解决方法就是给它创建一个home目录,运行:

sudo usermod --home /home/www-data www-data

毕竟里面将要存放密钥,安全起见,就取消全局可读的权限吧:

sudo chmod 750 /home/www-data

文档里的安全设置我暂时不打算搞了,等我什么时候把我的网站备案了再说吧。防火墙我已经在腾讯云的网页管理中心配置过了。首次手动部署至此完成!

自动化

发布的过程经常需要执行,修改代码后,当然想尽快试试新的成果啦。俗话说:工欲善其事,必先利其器。我通过Windows 10的Linux子系统功能执行Bash脚本来完成自动化发布。半分钟以内,一键完成

配置ssh连接使用public key认证

配置好这个,ssh连接时就不用输入密码了。

可以在腾讯云的网页中的管理中心配置连接用的密钥。当然,如果你在腾讯云中选择镜像的时就选择了ssh密钥,你应该已经有这个密钥文件了。把它复制到本地的~/.ssh/id.rsa即可。

或者,你也可以使用通用的方法配置,参见:使用Public Key (OpenSSH) 不用密码登陆,如果有提示权限问题,在指令的开头加入sudo即可。

配置sudo指令不用输入密码

如果你在腾讯云中选择镜像的时就选择了ssh密钥就可以跳过这步了,腾讯云会帮你进行这项配置。

连接到远处服务器,执行以下指令

sudo visudo

在最后添加一行

ubuntu  ALL=(ALL:ALL) NOPASSWD: ALL

ubuntu替换成你的用户名,顺便说一下Ctrl+O再按回车保存,Ctrl+X退出,最下面两行提示中的^Ctrl键的意思。

这个文件里的内容修改,特别是删除内容要慎重,我就因为把这个文件中的某一行注释掉了,导致我无法使用sudo命令了,最后重装系统才解决。

脚本

至此我们已经扫除了一切障碍,整个部署流程都不用输入密码了。就剩编写脚本了!

#!/bin/bash

restart=true

#Read parameters
while :; do
	case $1 in
		--no-restart)
			restart=false;;
		*)
			break;;
	esac
	shift
done

localDir="bin/publishToRemote/"
dotnet publish --configuration Release --output "$localDir" || { echo Abort.; exit 1; }

remoteTarget="/var/aspnetcore/myblog/"
server="ubuntu@huww98.cn"
echo Starting publish to $server:$remoteTarget

if [ $restart = true ]; then
	echo Stoping remote service
	ssh $server sudo service kestrel-myblog stop
fi

echo transfering files
rsync -rz "$localDir" "$server:$remoteTarget"

echo setting permissions
ssh $server \
"sudo chown -R www-data:www-data \"$remoteTarget\";
sudo chmod -R 775 \"$remoteTarget\";

if [ $restart = true ]; then
	echo restarting service
	sudo service kestrel-myblog start;
fi"

echo Published to remote successfully.

这段脚本的作用就是完全自动化程序编译,停止和重启服务,把新文件传输到服务器上。写这脚本看起来简单,实际上花了我这个新手一天多的时间呢。为你推荐一个学习脚本编写的好去处:Bash Guide,如果你愿意看英文教程的话。

这个脚本有一个参数--no-restart,可以控制不要重启服务。那既然自动化了,为何不更彻底一点呢?我还弄了个自动补全,这也可以写脚本实现。详见另一篇博文Bash自动补全


返回文章列表

评论

skyhope 2017/2/28 下午8:09:06

6666666662333

回复

梁月 2017/3/1 下午8:15:06

路过

回复

yangyangyang 2017/4/20 下午2:39:09

666

回复

寒木燨阳 2017/6/19 下午8:12:57

66666膜一下

回复

huww98 2017/6/19 下午8:28:49

这么大字干嘛?

回复

寒木燨阳 2017/6/19 下午10:39:22

就是有这种操作

回复

梁月 2017/9/24 下午5:30:09

Why I can not see my head ?

回复

⨍⨎⨏⨐⨑⨒⨓⨔⨕⨖⨗⨘ 2018/5/9 下午9:01:16

hhh
回复

⨍⨎⨏⨐⨑⨒⨓⨔⨕⨖⨗⨘ 2018/5/9 下午9:25:28

回复

登录 / 注册 后发布评论