最近在搞部署环节,我就先写这一部分吧。我打算介绍一下我把我的网站部署到腾讯云的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自动补全。