一、前言
在之前的文章(ASP.NET Core 實戰:Linux 小白的 .NET Core 部署之路)中,我介紹了如何在 Linux 環境中安裝 .NET Core SDK / .NET Core Runtime、Nginx、MySQL,以及如何將我們的 ASP.NET Core MVC 程序部署到 Linux 上,同時,使用 supervisor 守護程序守護我們的 .NET Core 程序。如果,你有看過那篇文章,並且和我一樣是個 Linux 小白用戶的話,可能第一感覺就是,把 .NET Core 項目部署在 IIS 上也挺好。
將 .NET Core 項目部署到 Linux 上如此復雜,就沒有簡單的部署方式嗎?
你好,有的,Docker 了解一下~~~
PS:這里的示例代碼還是采用之前的畢業設計項目,在這篇文章發布的時候,我已經在程序的倉庫中添加了對於 Docker 的支持,你可以下載下來,自己嘗試一下,畢竟,實踐出真知。
代碼倉儲:https://github.com/Lanesra712/Danvic.PSU
二、Step by Step
1、安裝 Docker & Docker Compose
在代碼交付的過程中,偶爾會遇到這樣的問題,在本地測試是好的,但是部署到測試環境、生產環境時就出這樣那樣的問題,同時,因為本地與測試環境、生產環境之間存在差異,我們可能無法在本地復現這些問題,那么,有沒有一種工具可以很好的解決這一問題呢?隨着歷史的車輪不斷前行,容器技術誕生了。
Docker,作為最近幾年興起的一種虛擬化容器技術,他可以將我們的運行程序與操作系統做一個隔離,例如這里我們需要運行 .NET Core 程序,我們不再需要關心底層的操作系統是什么,不需要在每台需要需要運行程序的機器上安裝程序運行的各種依賴,我們可以通過程序打包成鏡像的方式,將應用程序和該程序的依賴全部置於一個鏡像文件中,這時,只要別的機器上有安裝 Docker,就可以通過我們打包的這個鏡像來運行這個程序。
1.1、卸載 Docker
在安裝 Docker 之前,我們應該確定當前的機器上是否已經安裝好了 Docker,為了防止與現在安裝的 Docker CE 發生沖突,這里我們先卸載掉以前版本的 Docker,如果你確定你的機器上並沒有安裝 Docker 的話此步可以跳過。
在 Linux 中可以使用 \ 加 Enter 在輸入很長很長的語句時進行換行,這里和后面的命令都是采用這樣的方式。
sudo yum remove docker \ docker-client \ docker-client-latest \ docker-common \ docker-latest \ docker-latest-logrotate \ docker-logrotate \ docker-engine
1.2、添加 yum 源
在安裝 Docker CE 的方式上,我是采用將 Docker CE 的源添加到 yum 源中,之后我們就可以直接使用 yum install 安裝 Docker CE,整個的安裝過程如下。
# 安裝工具包從而可以讓我們在 yum 中添加別的倉儲源 sudo yum install -y yum-utils \ device-mapper-persistent-data \ lvm2 # 設置 docker ce 的穩定庫地址 sudo yum-config-manager \ --add-repo \ https://download.docker.com/linux/centos/docker-ce.repo # 安裝 docker ce sudo yum install docker-ce docker-ce-cli containerd.io
當我們安裝好 Docker 之后,我們就可以使用 docker 命令驗證我們是否在機器上成功安裝了 Docker,同時,也可以使用 docker --version 命令查看我們安裝的 Docker CE 版本。
1.3、設置開機自啟
當 Docker 已經在我們的機器上安裝完成后,我們就可以將 Docker 設置成機器的自啟服務,這樣,如果出現服務器重啟的情況下,我們的 Docker 也可以隨服務器的重啟自動啟動 Docker 服務。
# 啟動 Docker 服務並允許開機自啟 sudo systemctl start docker # 查看當前 dokcer 的運行情況 sudo systemctl status docker
1.4、Hello World
就像我們在學習一門新的語言時,運行的第一句代碼,幾乎都是打印出 Hello World,而在 Docker Hub 中,也有這么一個鏡像,在無數的 Docker 教程中,安裝完 Docker 后,第一件事就是拉取這個鏡像文件,“告訴” Docker,我來了。
Docker Hub 是存放鏡像的倉庫,里面包含了許多的鏡像文件,因為服務器在國外的原因,下載的速度可能不理想,像國內的阿里雲、騰訊雲也有提供對於 Docker 鏡像的加速器服務,你可以按需使用,當然,你也可以創建屬於你的私有鏡像倉庫。
docker run hello-world
docker run 命令,它會在我們的本地鏡像庫中先尋找這個鏡像,然后運行。如果在本地沒有找到的話,則會自動使用 docker pull 從 Docker Hub 中尋找,能找到的話,則會自動下載到本地,然后運行,找不到的話,這條命令也就運行失敗了。
1.5、安裝 Docker Compose
在實際的項目開發中,我們可能會有多個應用鏡像,例如在本篇文章的示例中,為了在 Docker 中運行我們的程序,我們需要三個鏡像:應用程序自身鏡像、MySQL Server 鏡像、以及 Nginx 鏡像,為了將我們的程序啟動起來,我們需要手敲各個容器的啟動參數,環境變量,容器命名,指定不同容器的鏈接參數等等一系列的操作,又多又煩,可能某一步操作失敗后程序就無法正常運行。而當我們使用了 Docker Compose 之后,我們就可以把這些命令一次性寫在 docker-compose.yml 配置文件中,以后每次啟動我們的應用程序時,只需要通過 docker compose 命令就可以自動幫我們完成這些操作。
# 從 github 下載 docker compose 二進制文件 sudo curl -L "https://github.com/docker/compose/releases/download/1.23.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose # 對下載的二進制文件應用可執行權限 sudo chmod +x /usr/local/bin/docker-compose # 查看 docker compose 版本 docker-compose --version
2、構建程序鏡像
當我們在服務器上安裝好 docker 和 docker compose 之后,就可以開始構建我們的程序鏡像了。首先我們需要對我們的運行程序添加對於 Docker 的支持。你可以自己手動在 MVC 項目中添加 Dockerfile 文件,或是通過右鍵添加 Docker 支持。
Dockerfile 就像一個執行的清單,它告訴 Docker,我們這個鏡像在構建和運行時需要按照什么樣的命令運行。打開 VS 為我們自動創建的 Dockerfile,可以看到清晰的分成了四塊的內容。
我們知道,.NET Core 程序的運行需要依賴於 .NET Core Runtime(CoreCLR),因此,為了使我們的程序可以運行起來,我們需要從 hub 中拉取 runtime ,並在 此基礎上構建我們的應用鏡像。同時,為了避免因為基礎的環境的不同造成對程序的影響,這里的 Runtime 需要同程序開發時的 .NET Core SDK 版本保持一致,所以這里我使用的是 .NET Core 2.1 Runtime。
一個鏡像中包含了應用程序及其所有的依賴,與虛擬機不同的是,容器中的每個鏡像最終是共享了宿主機的操作系統資源,容器作為用戶空間中的獨立進程運行在主機操作系統上。
PS:圖片版權歸屬於微軟的技術文檔,如有侵權,請聯系我刪除,源文件地址:什么是 Docker?
鏡像可以看成一個個小型的“虛擬主機”,這里我們在鏡像中創建了一個 /app 路徑作為我們程序在鏡像中的工作目錄,同時,將 80 端口暴露給 Docker,從而可以使我們在鏡像外面通過端口訪問到當前鏡像中的運行的程序。
FROM microsoft/dotnet:2.1-aspnetcore-runtime AS base WORKDIR /app EXPOSE 80 EXPOSE 443
因為我們的應用是一個多層架構的單體應用,最終的 MVC 項目依賴於解決方案中的各個類庫以及我們從 Nuget 中下載的各種第三方組件,在部署時,需要將這些組件打包成 dll 引用。所以,這里我們需要使用 .NET Core SDK 中包含的 .NET Core CLI 進行還原和構建。
就像在下面的代碼中,我們在鏡像的內部創建了一個 /src 的路徑,將當前解決方案下的類庫都復制到這個目錄下,之后通過 dotnet restore 命令還原我們的主程序所依賴的各個組件。當我們還原好依賴的組件后,就可以使用 dotnet build 命令生成 Release版本的 dll 文件,同時輸出到之前創建的 /app 路徑下。
FROM microsoft/dotnet:2.1-sdk AS build WORKDIR /src COPY ["PSU.Site/PSU.Site.csproj", "PSU.Site/"] COPY ["03_Logic/PSU.Domain/PSU.Domain.csproj", "03_Logic/PSU.Domain/"] COPY ["03_Logic/PSU.Repository/PSU.Repository.csproj", "03_Logic/PSU.Repository/"] COPY ["01_Entity/PSU.Entity/PSU.Entity.csproj", "01_Entity/PSU.Entity/"] COPY ["02_Infrastructure/PSU.Utility/PSU.Utility.csproj", "02_Infrastructure/PSU.Utility/"] COPY ["04_Rule/PSU.Model/PSU.Model.csproj", "04_Rule/PSU.Model/"] COPY ["02_Infrastructure/PSU.EFCore/PSU.EFCore.csproj", "02_Infrastructure/PSU.EFCore/"] COPY ["04_Rule/PSU.IService/PSU.IService.csproj", "04_Rule/PSU.IService/"] COPY ["Controllers.PSU/Controllers.PSU.csproj", "Controllers.PSU/"] RUN dotnet restore "PSU.Site/PSU.Site.csproj" COPY . . WORKDIR "/src/PSU.Site" RUN dotnet build "PSU.Site.csproj" -c Release -o /app
上面一步可以看成我們在使用 VS 生成 Release 版本的解決方案,當生成沒有出錯之后,我們就可以進行程序的發布。
FROM build AS publish RUN dotnet publish "PSU.Site.csproj" -c Release -o /app
當已經生成發布文件之后,按照我們平時部署在 Windows 上的過程,這時就可以通過 IIS 部署運行了,因此,構建我們應用鏡像的最后一步就是通過 dotnet 命令執行我們的程序。
FROM base AS final WORKDIR /app COPY --from=publish /app . ENTRYPOINT ["dotnet", "PSU.Site.dll"]
似乎到這一步構建程序鏡像就結束了,按照這樣流程做的話,就需要我們將整個的解決方案上傳到服務器上了,可是,很多時候,我們僅僅是把我們在本地發布好的項目上傳到服務器上,這與我們現在的構建流程具有很大的不同,所以這里我們來修改 Dockerfile 文件,從而符合我們的發布流程。
從上面分析 Dockerfile 的過程中不難看出,在服務器上構建鏡像的第二步、第三步就是我們現在在開發環境中手動完成的部分,所以這里,我們只需要對這部分進行刪除即可,修改后的 Dockerfile 如下。
FROM microsoft/dotnet:2.1-aspnetcore-runtime WORKDIR /app COPY . /app EXPOSE 80 ENTRYPOINT ["dotnet","PSU.Site.dll"]
在修改后的 Dockerfile 中,可以看到,我們刪去了 build 和 release 的過程,選擇直接將我們 Dockerfile 路徑下的文件拷貝到鏡像中的 /app 路徑下,然后直接執行 dotnet 命令,運行我們的程序。
為了確保 Dockerfile 與發布后的文件處於同一路徑下,這里我們需要使用 VS 修改 Dockerfile 的屬性值,確保會復制到輸出的目錄下,這里選擇如果較新則復制即可。
3、編寫 docker-compose.yml
當我們構建好應用的鏡像,對於 Nginx 和 MySQL 我們完全可以從 hub 中拉取下來,再執行一些配置即可。所以,我們現在就可以編寫 docker compose 文件,來定義我們的應用鏡像運行時需要包含的依賴以及每個鏡像的啟動順序。
右鍵選中 MVC 項目,添加一個 docker-compose.yml 文件,同樣的,需要修改該文件的屬性,以便於該文件可以復制到輸出目錄下。注意,這里的文件名和上文的 Dockerfile 都是特定的,你不能做任何的修改。如果你的電腦上已經安裝了 Docker for Windows,你也可以使用 VS,右鍵添加,選中容器業務流程協調程序支持自動對 docker compose 進行配置。
在 yml 文件中,我定義了三個鏡像:psu.site、docker.mysql、docker.nginx。三個鏡像的定義中有許多相同的地方,都設置了自動重啟(restart),以及都處於同一個橋接網絡下(psu-net)從而達到鏡像間的通信。
docker.mysql 是 MySQL 的鏡像,我們通過環境變量 MYSQL_ROOT_PASSWORD 設置了 MySQL 的數據庫連接密碼,並通過掛載卷的方式將鏡像中的數據庫文件持久化到我們的服務器本地路徑中。同時,將鏡像的 3306 端口映射到服務器的 3306 端口上。
psu.site 則是我們的程序鏡像,采用位於 /usr/wwwroot/psu/ 路徑下的 Dockerfile 文件進行構建的,因為主程序的運行需要依賴於數據庫,所以這里采用 depends_on 屬性,使我們的應用鏡像依賴於 docker.mysql 鏡像,即,在 docker.mysql 啟動后才會啟動應用鏡像。
docker.nginx 則是我們的 nginx 鏡像,這里將鏡像中的 80 端口和 443 端口都映射到服務器 IP 上,因為我們需要配置 Nginx 從而監聽我們的程序,所以通過掛載卷的方式,將本地的 nginx.conf 配置文件用配置映射到鏡像中。同時,因為我們在構建應用鏡像的 Dockerfile 文件時,對外暴露了 80 端口,所以這里就可以通過 links 屬性進行監聽(如果構建時未暴露端口,你可以在 docker compose 文件中通過 Expose 屬性暴露鏡像中的端口)。
Nginx 的配置文件如下,這里特別需要注意文件的格式,縮進,一點小錯誤都可能導致鏡像無法正常運行。如果你和我一樣將 nginx.conf 放到程序運行路徑下的,別忘了修改文件的屬性。
server { listen 80; location / { proxy_pass http://psu.site; proxy_http_version 1.1; proxy_set_header Upgrade $http_upgrade; proxy_set_header Host $http_host; proxy_cache_bypass $http_upgrade; } }
一個完整的 docker compose 文件如下,包含了三個鏡像以及一個橋接網絡。
version: '3.7' services: docker.mysql: image: mysql ports: - "3306:3306" restart: always environment: - MYSQL_ROOT_PASSWORD=123456@sql volumes: - /usr/mysql:/var/lib/mysql networks: - psu-net psu.site: build: /usr/wwwroot/psu/ restart: always depends_on: - docker.mysql networks: - psu-net docker.nginx: image: nginx ports: - "80:80" - "443:443" volumes: - ./nginx.conf:/etc/nginx/nginx.conf links: - psu.site networks: - psu-net networks: psu-net: driver: bridge
這里需要注意,所有有用到鏡像間的通信的地方,我們都需要使用鏡像名進行指代,例如上面的 nginx 的配置文件中,我們需要將監聽的地址改為鏡像名稱,以及,我們需要修改程序的數據庫訪問字符串的服務器地址,修改后的數據庫連接字符串如下所示。
"ConnectionStrings": { "SQLConnection": "server=docker.mysql;database=PSU.Site;user=root;password=123456@sql;port=3306;persistsecurityinfo=True;" }
4、發布部署程序
當我們構建好 docker compose 文件后就可以把整個文件上傳到服務器上進行構建 docker 鏡像了。這里我將所有的部署文件放在服務器的 /usr/wwwroot/psu/ 路徑下,這時我們就可以通過 docker compose 命令進行鏡像構建。
定位到部署文件在的位置,我們可以直接使用下面的命令進行鏡像的(重新)構建,啟動,並鏈接一個服務相關的容器,整個過程都會在后台運行,如果你希望看到整個過程的話,你可以去掉 -d 參數。
# 執行鏡像構建,啟動 docker-compose up -d
當 up 命令執行完成后,我們就可以通過 ps 命令查看正在運行的容器,若有的容器並沒有運行起來,則可以使用 logs 查看容器的運行日志從而進行排錯。
# 查看所有正在運行的容器 docker-compose ps # 顯示容器運行日志 docker-compose logs
三、總結
本章主要是介紹了如何通過 docker 容器,完整的部署一個可實際使用的 .NET Core 的單體應用,相比於之前通過 Linux 部署 .NET Core 應用,可以看到整個步驟少了很多,也簡單很多。文中涉及到了一些 docker 的命令,如果你之前並沒有接觸過 docker 的話,可能需要你進一步的了解。當我們將程序打包成一個鏡像之后,你完全可以將鏡像上傳到私有鏡像倉庫中,或是直接打包成鏡像的壓縮文件,這樣,當需要切換部署環境時,只需要獲取到這個鏡像之后即可快速完成部署,相比之前,極大的方便了我們的工作。