2020年8月21日 星期五

在 Linux Ubuntu(16.04) 佈署 ASP.NET Core 應用程式至 Nginx 伺服器

在採用 Nginx 來取代 IIS 作為 ASP.NET Core 應用程式的反向代理伺服器(Reverse proxy)過程中,覺得過程有點繁瑣,因此在這邊記錄一下從頭到尾的架設步驟及相關的疑難排解,提供給下一次安裝參考用。


1. 安裝 .NET Core SDK 或 .NET Core Runtime

先將 Microsoft 的套件簽署金鑰新增至受信賴金鑰清單及加入套件存放庫,依序執行以下命令

$ wget https://packages.microsoft.com/config/ubuntu/16.04/packages-microsoft-prod.deb -O packages-microsoft-prod.deb

$ sudo dpkg -i packages-microsoft-prod.deb

$ sudo apt-get update

$ sudo apt-get install -y apt-transport-https

$ sudo apt-get update

接著在看是選擇要安裝 .NET Core SDK 還是 .NET Core Runtime 都可以,自行選擇一種方式(SDK 提供的功能較多,主要是用來開發除錯用,但體積較大,一般來說裝 Runtime 即可)

/* 1. 安裝 .NET Core SDK */  
$ sudo apt-get install -y dotnet-sdk-3.1

/* 2. 安裝 .NET Core Runtime */  
$ sudo apt-get install -y aspnetcore-runtime-3.1

/* 補充: 安裝不包含 ASP.NET Core 支援的 .NET Core Runtime */
$ sudo apt-get install -y dotnet-runtime-3.1 

到這邊其實就已經完成 .NET Core 執行環境的安裝了,但如果執行了上面的命令遇到了類似 Unable to locate package {netcore-package} 這樣的訊息時(我就真的遇到了),那麼請參考 疑難排解#1

補充: 若之後要更新 SDK 或 Runtime,執行以下命令即可

# Step 1
$ sudo apt-get update

# Step 2    
$ sudo apt-get upgrade



2. 安裝 Nginx

使用編輯器開啟/etc/apt/sources.list並加入下面兩行 Nginx 套件來源節點(stanza)

deb https://nginx.org/packages/ubuntu/ xenial nginx        # Ubuntu 版本為 18.04 的話 xenial -> bionic
deb-src https://nginx.org/packages/ubuntu/ xenial nginx    # Ubuntu 版本為 18.04 的話 xenial -> bionic

開始安裝 Nginx

$ sudo apt-get update
$ sudo apt-get install nginx

到這邊就已完成 Nginx 的安裝,但若出現 GPG error: https://nginx.org/packages/ubuntu xenial Release: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY $key 的錯誤訊息,請參考 疑難排解#2

3. 設定 Nginx

先試著啟動 Nginx

$ sudo service nginx start  

然後在瀏覽器上輸入 Server IP 應該就能看到預設畫面

Image

根據微軟官方文件上的描述,安裝完成後在 /etc/nginx/sites-available 的路徑下應該會有一個名為default.conf的設定檔,但我的default.conf是在 conf.d 資料夾內,然後也沒有 sites-available 資料夾,猜想 Nginx 官方可能有做了些修改,因此我是在 /etc/nginx 資料夾下先自行建立 sites-available 資料夾然後在裡面新增一個your_app_name.conf檔(名稱可修改成你想取的)內容如下

server {
    listen 80;
    listen [::]:80;

    # SSL 憑證設定方式(有需要的話把以下四行的註解拿掉並指向自己放憑證的目錄)
    # listen 443 ssl http2;
    # listen [::]:443 ssl http2;
    # ssl_certificate /etc/nginx/ssl/your_domain_com/ssl-bundle.crt;
    # ssl_certificate_key /etc/nginx/ssl/your_domain_com/private.key;

    server_name  your_domain.com *.your_domain.com;  # 你的網域名稱(多個以空白分隔)

    location / {
        proxy_pass         http://localhost:5000;  # 根據你使用的 Port 調整
        proxy_http_version 1.1;        
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection keep-alive;
        proxy_set_header   Host $host;
        proxy_cache_bypass $http_upgrade;
        proxy_set_header   X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header   X-Forwarded-Proto $scheme;
    }

    # 如果有 SignalR 連線需求的話可參考以下設定(非必要)
    location /hub {
        proxy_pass         http://localhost:5000;
        proxy_http_version 1.1;
        proxy_set_header   Upgrade $http_upgrade;
        proxy_set_header   Connection $connection_upgrade;  # $connection_upgrade 該變數來自 nginx.conf
        proxy_cache off;
        proxy_buffering off;
        proxy_read_timeout 100s;
        proxy_set_header Host $host;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
        proxy_set_header X-Forwarded-Proto $scheme;
    }
}

補充: location 的匹配規則可以參考這

然後參考官方文件修改conf.d/default.conf的設定檔,改成

server {
    listen 80 default_server;  # 加上 default_server
    listen [::]:80 default_server deferred;  # IPv6 用,加上 deferred 為客戶端建立完 TCP 連接後,內核會等到有請求數據時才喚醒 worker 以減輕其負擔;適用於高併發場景
    server_name _;  # localhost 改成_
    return 444;     # 加上這行可讓外部直連 IP 的方式失效 

    #charset koi8-r;
    #access_log  /var/log/nginx/host.access.log  main;

    location / {
        root   /usr/share/nginx/html;
        index  index.html index.htm;
    }
}

最後再參考以下範例編輯/etc/nginx/nginx.conf檔並在最下面加入include /etc/nginx/sites-available/*.conf;這行設定,如下

user  nginx;
worker_processes  auto;  # 請設定與 CPU 核心數相等的數字,進程切換代價最小,auto 為系統自動偵測 CPU 核心數,預設為 1

error_log  /var/log/nginx/error.log warn;
pid        /var/run/nginx.pid;


events {
    worker_connections  1024;
    multi_accept on;  # 儘可能接受最多的連接數
    use epoll;
}

http {
    server_tokens off;  # 隱藏 Nginx 版本資訊
    include       /etc/nginx/mime.types;
    default_type  application/octet-stream;
    client_max_body_size 10M;  # 上傳檔案大小限制(10MB)

    ssl_protocols TLSv1.2 TLSv1.3;
    ssl_prefer_server_ciphers on;
    ssl_ciphers "EECDH+AESGCM:EDH+AESGCM:AES256+EECDH:AES256+EDH";
    ssl_ecdh_curve secp384r1;

    log_format  main  '$remote_addr - $remote_user [$time_local] "$request" '
                      '$status $body_bytes_sent "$http_referer" '
                      '"$http_user_agent" "$http_x_forwarded_for"';

    access_log  /var/log/nginx/access.log  main;

    sendfile on;
    tcp_nopush on;
    tcp_nodelay on;

    keepalive_timeout 30s;
    client_header_timeout 30s;
    client_body_timeout 30s;
    reset_timedout_connection on;
    send_timeout 30s;

    # Gzip 設置
    gzip on;
    gzip_proxied any;
    gzip_buffers 16 8k;
    gzip_disable "MSIE [1-6]\.(?!.*SV1)";
    gzip_http_version 1.1;
    gzip_vary on;          # 增加回應標頭 "Vary: Accept-Encoding"    
    gzip_comp_level 6;     # 壓縮比1~9, 數字越高壓縮程度越大但越耗 CPU 效能
    gzip_min_length 1024;  # 超過 1024k 才壓縮, 預設是 0 全壓縮
    gzip_types text/plain text/css application/javascript application/x-javascript text/javascript application/json application/xml application/xml+rss text/xml;

    # 根據 $http_connection 來決定變數 $connection_upgrade 的值(SignalR 會用到)
    map $http_connection $connection_upgrade {
        "~*Upgrade" $http_connection;
        default keep-alive;
    }
    proxy_buffering off;
    proxy_set_header Host $host;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection $connection_upgrade;
    proxy_set_header X-Real-IP $remote_addr;
    proxy_set_header X-Forwarded-Proto $scheme;
    proxy_set_header X-Forwarded-Server $host;
    proxy_set_header X-Forwarded-Host $host:$server_port;
    proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    proxy_cache_bypass $http_upgrade;
    proxy_http_version 1.1;

    include /etc/nginx/conf.d/*.conf;
    include /etc/nginx/sites-available/*.conf;  # 加入這行
}

以上設定都完成後,執行以下命令

# 驗證設定檔
$ sudo nginx -t

# 讓 Nginx 重新讀取新的設定檔 
$ sudo nginx -s reload



4. 發佈及佈署 ASP.NET Core 應用程式

在反向代理(Reverse Proxy)模式下,使用 HttpContext.Connection.RemoteIpAddress 會抓到不正確的客戶端 IP,因此要在網站專案下的Startup.cs檔裡加入 app.UseForwardedHeaders(),其置放的位置根據微軟官方文件的建議,要在診斷和處理錯誤的中介服務(Middleware)後,其它服務前,如下

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
    if (env.IsDevelopment())
    {
       app.UseDeveloperExceptionPage();
       app.UseForwardedHeaders(new ForwardedHeadersOptions
       {
           ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
       });
    }
    else
    {
       app.UseExceptionHandler("/Home/Error");
       app.UseForwardedHeaders(new ForwardedHeadersOptions
       {
           ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto
       });
       app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();

    /*** 省略其它代碼 ***/
}

由於我們已經要使用 Nginx 了,SSL 相關的憑證到時都會直接綁在 Nginx 上,所以要在我們的 ASP.NET Core 應用程式上移掉有關 https 的設定,可參考此篇文章設定 ASP.NET Core 3.x 的起始連結(URL)位址的說明,修改 Properties 目錄下launchSettings.json 檔案裡的 applicationUrl 屬性值,將 https://localhost:xxxx 移除(如果有),或是直接在相對應的appsettings.json裡只設定 Http 區段,如下

{
  "Logging": {
    "LogLevel": {
      "Default": "Warning",
      "Microsoft": "Warning",
      "Microsoft.Hosting.Lifetime": "Information"
    }
  },
  "Kestrel": {         
    "EndPoints": {
      "Http": {
        "Url": "http://localhost:5000"  /* 只保留 HTTP 這行 */
      }
    }
  },
  "ConnectionStrings": {
    "MyDbContext": ""
  },
  /*** 省略其它代碼 ***/
}

然後使用以下的指令發佈應用程式(二擇一)

# 1. 不指定特地平台  
$ dotnet publish -c Release -o bin\Release\netcoreapp3.1\WebPublisher

# 2. 有指定特地平台  
$ dotnet publish -c Release -r linux-x64 --self-contained false -o bin\Release\netcoreapp3.1\WebPublisher

將產生的檔案(WebPublisher 資料夾下的所有檔案)複製到伺服器上的/var/www/your_app_name下,並執行

$ dotnet Your_App_Assembly_Name.dll

補充: 若有指定環境變數的需求,可先輸入以下指令來改變目前應用程式的執行環境變數

$ export ASPNETCORE_ENVIRONMENT=YourEnvironment

或是直接在執行時指定環境變數

$ dotnet Your_App_Assembly_Name.dll --environment=YourEnvironment # e.g. --environment=Uat



5. 將 ASP.NET Core 應用程式註冊為系統服務

為了讓 ASP.NET Core 應用程式在系統重開機後都能自動啟動,還需要再進行一些設定,首先檢查系統是否已有 www-data 群組

# 1. 確認 www-data 群組是否存在
$ grep 'www-data' /etc/group

# 2. 若不存在才執行此行命令  
$ sudo groupadd www-data

# 3. 將網站資料夾的群組擁有者設給 www-data 
$ sudo chgrp -R www-data /var/www/your_app_name

# 4. 讓群組有讀寫權限
$ sudo chmod 775 -R /var/www/your_app_name

再來切換到 /etc/systemd/system 目錄下,建立一個系統服務定義檔

$ sudo nano /etc/systemd/system/your_app_name.service  

編輯該檔,內容如下

[Unit]
Description=Your App Name  # 你的應用程式名稱

[Service]
WorkingDirectory=/var/www/your_app_name  # 你的應用程式路徑
ExecStart=/usr/bin/dotnet /var/www/your_app_name/Your_App_Assembly_Name.dll  # 改成你的 dll 檔
Restart=always
RestartSec=10  # 假如應用程式遇到錯誤而停止,10 秒後將會自動重啟
KillSignal=SIGINT
SyslogIdentifier=your_app_name  # your_app_name 改成你的應用程式名稱(Log用)
User=www-data
Environment=ASPNETCORE_ENVIRONMENT=Production
Environment=DOTNET_PRINT_TELEMETRY_MESSAGE=false

[Install]
WantedBy=multi-user.target

最後依序執行下列步驟 1 ~ 3 的指令

# 1. 開啟系統開機自動啟動服務  
$ sudo systemctl enable your_app_name.service

# 2. 啟動服務  
$ sudo systemctl start your_app_name.service

# 3. 查看狀態  
$ sudo systemctl status your_app_name.service


# 補充
$ sudo systemctl disable your_app_name.service  # 關閉系統開機自動啟動服務
$ sudo systemctl stop your_app_name.service    # 停止服務 
$ sudo systemctl restart your_app_name.service  # 重啟服務

看到出現類似下面畫面即大功告成

Image

補充: 使用以下指令可查看系統服務的日誌

$ sudo journalctl -fu your_app_name.service  

$ sudo journalctl -fu your_app_name.service --since "2020-08-30" --until "2020-08-31 18:00"  # 指定區間  




疑難排解#1

當遇到了Unable to locate package {netcore-package}的訊息時,請試著依序執行以下命令

$ sudo dpkg --purge packages-microsoft-prod && sudo dpkg -i packages-microsoft-prod.deb

$ sudo apt-get update

$ sudo apt-get install {dotnet-package}  # {dotnet-package} = dotnet-sdk-3.1 或 aspnetcore-runtime-3.1 或 dotnet-runtime-3.1 

若以上的方式還是沒有辦法解決問題,就只能再執行以下命令進行手動安裝了

$ sudo apt-get install -y gpg

$ wget -O - https://packages.microsoft.com/keys/microsoft.asc | gpg --dearmor -o microsoft.asc.gpg

$ sudo mv microsoft.asc.gpg /etc/apt/trusted.gpg.d/

$ wget https://packages.microsoft.com/config/ubuntu/{os-version}/prod.list  # {os-version} = 16.04 或 其它你使用的版號

$ sudo mv prod.list /etc/apt/sources.list.d/microsoft-prod.list

$ sudo chown root:root /etc/apt/trusted.gpg.d/microsoft.asc.gpg

$ sudo chown root:root /etc/apt/sources.list.d/microsoft-prod.list

$ sudo apt-get update

$ sudo apt-get install -y apt-transport-https

$ sudo apt-get update

$ sudo apt-get install -y {dotnet-package}  # {dotnet-package} = dotnet-sdk-3.1 或 aspnetcore-runtime-3.1 或 dotnet-runtime-3.1

疑難排解#2

當安裝 Nginx 出現類似GPG error: https://nginx.org/packages/ubuntu xenial Release: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY $key的錯誤訊息時,請試著依序執行以下命令

$ sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys $key  # $key 的值在 GPG 錯誤訊息裡
$ sudo apt-get update
$ sudo apt-get install nginx




參考資料

[Microsoft] Install .NET Core SDK or .NET Core Runtime on Ubuntu

[Microsoft] Host ASP.NET Core on Linux with Nginx

[NGINX] Install NGINX

訪客統計

103046