天天看點

通過Docker在Linux上托管.NET Core

通過Docker在Linux上托管.NET Core

這篇文章基于我之前的文章 .net core 入門。首先,我把 restful api 從 .net core rc1 更新到了 .net core 1.0,然後,我增加了對 docker 的支援并描述了如何在 linux 生産環境裡托管它。

我是首次接觸 docker 并且距離成為一名 linux 高手還有很遠的一段路程。是以,這裡的很多想法是來自一個新手。

安裝

按照 https://www.microsoft.com/net/core 上的介紹在你的電腦上安裝 .net core 。這将會同時在 windows 上安裝 dotnet 指令行工具以及最新的 visual studio 工具。

源代碼

你可以直接到 github 上找最到最新完整的源代碼。

轉換到 .net core 1.0

自然地,當我考慮如何把 api 從 .net core rc1 更新到 .net core 1.0 時想到的第一個求助的地方就是谷歌搜尋。我是按照下面這兩條非常全面的指導來進行更新的:

從 dnx 遷移到 .net core cli

從 asp.net 5 rc1 遷移到 asp.net core 1.0

當你遷移代碼的時候,我建議仔細閱讀這兩篇指導,因為我在沒有閱讀第一篇指導的情況下又嘗試浏覽第二篇,結果感到非常迷惑和沮喪。

我不想描述細節上的改變因為你可以看 github 上的送出。這兒是我所作改變的總結:

更新 global.json 和 project.json 上的版本号

删除 project.json 上的廢棄章節

使用輕型 controllerbase 而不是 controller, 因為我不需要與 mvc 視圖相關的方法(這是一個可選的改變)。

從輔助方法中去掉 http 字首,比如:httpnotfound -> notfound

logverbose -> logtrace

名字空間改變: microsoft.aspnetcore.*

在 startup 中使用 setbasepath(沒有它 appsettings.json 将不會被發現)

通過 webhostbuilder 來運作而不是通過 webapplication.run 來運作

删除 serilog(在寫文章的時候,它不支援 .net core 1.0)

唯一令我真正頭疼的事是需要移動 serilog。我本可以實作自己的檔案記錄器,但是我删除了檔案記錄功能,因為我不想為了這次操作在這件事情上花費精力。

不幸的是,将有大量的第三方開發者扮演追趕 .net core 1.0 的角色,我非常同情他們,因為他們通常在休息時間還堅持工作但卻依舊根本無法接近靠攏微軟的可用資源。我建議閱讀 travis illig 的文章 .net core 1.0 釋出了,但 autofac 在哪兒?這是一篇關于第三方開發者觀點的文章。

做了這些改變以後,我可以從 project.json 目錄恢複、建構并運作 dotnet,可以看到 api 又像以前一樣工作了。

通過 docker 運作

在我寫這篇文章的時候, docker 隻能夠在 linux 系統上工作。在 windows 系統和 os x 上有 beta 支援 docker,但是它們都必須依賴于虛拟化技術,是以,我選擇把 ubuntu 14.04 當作虛拟機來運作。如果你還沒有安裝過 docker,請按照指導來安裝。

我最近閱讀了一些關于 docker 的東西,但我直到現在還沒有真正用它來幹任何事。我假設讀者還沒有關于 docker 的知識,是以我會解釋我所使用的所有指令。

hello docker

在 ubuntu 上安裝好 docker 之後,我所進行的下一步就是按照https://www.microsoft.com/net/core#docker 上的介紹來開始運作 .net core 和 docker。

首先啟動一個已安裝有 .net core 的容器。

docker run -it microsoft/dotnet:latest 

-it 選項表示互動,是以你執行這條指令之後,你就處于容器之内了,可以如你所希望的那樣執行任何 bash 指令。

然後我們可以執行下面這五條指令來在 docker 内部運作起來微軟 .net core 控制台應用程式示例。

mkdir hwapp

cd hwapp

dotnet new

dotnet restore

dotnet run 

你可以通過運作 exit 來離開容器,然後運作 docker ps -a 指令,這會顯示你建立的那個已經退出的容器。你可以通過上運作指令 docker rm <container_name> 來清除容器。

挂載源代碼

我的下一步驟是使用和上面相同的 microsoft/dotnet 鏡像,但是将為我們的應用程式以資料卷的方式挂載上源代碼。

首先簽出有相關送出的倉庫:

git clone https://github.com/niksoper/aspnet5-books.git 

cd aspnet5-books/src/mvclibrary 

git checkout dotnet-core-1.0 

現在啟動一個容器來運作 .net core 1.0,并将源代碼放在 /book 下。注意更改 /path/to/repo 這部分檔案來比對你的電腦:

docker run -it \ 

-v /path/to/repo/aspnet5-books/src/mvclibrary:/books \ 

microsoft/dotnet:latest 

現在你可以在容器中運作應用程式了!

cd /books 

dotnet restore 

作為一個概念性展示這的确很棒,但是我們可不想每次運作一個程式都要考慮如何把源代碼安裝到容器裡。

增加一個 dockerfile

我的下一步驟是引入一個 dockerfile,這可以讓應用程式很容易在自己的容器内啟動。

我的 dockerfile 和 project.json 一樣位于 src/mvclibrary 目錄下,看起來像下面這樣:

from microsoft/dotnet:latest 

# 為應用程式源代碼建立目錄 

run mkdir -p /usr/src/books 

workdir /usr/src/books 

# 複制源代碼并恢複依賴關系 

copy . /usr/src/books 

run dotnet restore 

# 暴露端口并運作應用程式 

expose 5000 

cmd [ "dotnet", "run" ] 

嚴格來說,run mkdir -p /usr/src/books 指令是不需要的,因為 copy 會自動建立丢失的目錄。

docker 鏡像是按層建立的,我們從包含 .net core 的鏡像開始,添加另一個從源代碼生成應用程式,然後運作這個應用程式的層。

添加了 dockerfile 以後,我通過運作下面的指令來生成一個鏡像,并使用生成的鏡像啟動一個容器(確定在和 dockerfile 相同的目錄下進行操作,并且你應該使用自己的使用者名)。

docker build -t niksoper/netcore-books . 

docker run -it niksoper/netcore-books 

你應該看到程式能夠和之前一樣的運作,不過這一次我們不需要像之前那樣安裝源代碼,因為源代碼已經包含在 docker 鏡像裡面了。

暴露并釋出端口

這個 api 并不是特别有用,除非我們需要從容器外面和它進行通信。 docker 已經有了暴露和釋出端口的概念,但這是兩件完全不同的事。

據 docker 官方文檔:

expose 指令通知 docker 容器在運作時監聽特定的網絡端口。expose 指令不能夠讓容器的端口可被主機通路。要使可被通路,你必須通過 -p 标志來釋出一個端口範圍或者使用 -p 标志來釋出所有暴露的端口

expose 指令隻是将中繼資料添加到鏡像上,是以你可以如文檔中說的認為它是鏡像消費者。從技術上講,我本應該忽略 expose 5000 這行指令,因為我知道 api 正在監聽的端口,但把它們留下很有用的,并且值得推薦。

在這個階段,我想直接從主機通路這個 api ,是以我需要通過 -p 指令來釋出這個端口,這将允許請求從主機上的端口 5000 轉發到容器上的端口 5000,無論這個端口是不是之前通過 dockerfile 暴露的。

docker run -d -p 5000:5000 niksoper/netcore-books 

通過 -d 指令告訴 docker 在分離模式下運作容器,是以我們不能看到它的輸出,但是它依舊會運作并監聽端口 5000。你可以通過 docker ps 來證明這件事。

是以,接下來我準備從主機向容器發起一個請求來慶祝一下:

curl http://localhost:5000/api/books 

它不工作。

重複進行相同 curl 請求,我看到了兩個錯誤:要麼是 curl: (56) recv failure: connection reset by peer,要麼是 curl: (52) empty reply from server。

我傳回去看 docker run 的文檔,然後再次檢查我所使用的 -p 選項以及 dockerfile 中的 expose 指令是否正确。我沒有發現任何問題,這讓我開始有些沮喪。

重新振作起來以後,我決定去咨詢當地的一個 scott logic devops 大師 - dave wybourn(也在這篇 docker swarm 的文章裡提到過),他的團隊也曾遇到這個實際問題。這個問題是我沒有配置過 kestral,這是一個全新的輕量級、跨平台 web 伺服器,用于 .net core 。

預設情況下, kestrel 會監聽 http://localhost:5000。但問題是,這兒的 localhost 是一個回路接口。

據維基百科:

在計算機網絡中,localhost 是一個代表本機的主機名。本地主機可以通過網絡回路接口通路在主機上運作的網絡服務。通過使用回路接口可以繞過任何硬體網絡接口。

當運作在容器内時這是一個問題,因為 localhost 隻能夠在容器内通路。解決方法是更新 startup.cs 裡的 main 方法來配置 kestral 監聽的 url:

public static void main(string[] args) 

  var host = new webhostbuilder() 

    .usekestrel() 

    .usecontentroot(directory.getcurrentdirectory()) 

    .useurls("http://*:5000") // 在所有網絡接口上監聽端口 5000 

    .useiisintegration() 

    .usestartup<startup>() 

    .build(); 

  host.run(); 

通過這些額外的配置,我可以重建鏡像,并在容器中運作應用程式,它将能夠接收來自主機的請求:

curl -i http://localhost:5000/api/books 

我現在得到下面這些相應:

http/1.1 200 ok 

date: tue, 30 aug 2016 15:25:43 gmt 

transfer-encoding: chunked 

content-type: application/json; charset=utf-8 

server: kestrel 

[{"id":"1","title":"restful api with asp.net core mvc 1.0","author":"nick soper"}] 

在産品環境中運作 kestrel

微軟的介紹:

kestrel 可以很好的處理來自 asp.net 的動态内容,然而,網絡服務部分的特性沒有如 iis,apache 或者 nginx 那樣的全特性伺服器那麼好。反向代理伺服器可以讓你不用去做像處理靜态内容、緩存請求、壓縮請求、ssl 端點這樣的來自 http 伺服器的工作。

是以我需要在我的 linux 機器上把 nginx 設定成一個反向代理伺服器。微軟介紹了如何釋出到 linux 生産環境下的指導教程。我把說明總結在這兒:

通過 dotnet publish 來給應用程式産生一個自包含包。

把已釋出的應用程式複制到伺服器上

安裝并配置 nginx(作為反向代理伺服器)

安裝并配置 supervisor(用于確定 nginx 伺服器處于運作狀态中)

安裝并配置 apparmor(用于限制應用的資源使用)

配置伺服器防火牆

安全加強 nginx(從源代碼建構和配置 ssl)

這些内容已經超出了本文的範圍,是以我将側重于如何把 nginx 配置成一個反向代理伺服器。自然地,我通過 docker 來完成這件事。

在另一個容器中運作 nginx

我的目标是在第二個 docker 容器中運作 nginx 并把它配置成我們的應用程式容器的反向代理伺服器。

我使用的是來自 docker hub 的官方 nginx 鏡像。首先我嘗試這樣做:

docker run -d -p 8080:80 --name web nginx 

這啟動了一個運作 nginx 的容器并把主機上的 8080 端口映射到了容器的 80 端口上。現在在浏覽器中打開網址 http://localhost:8080 會顯示出 nginx 的預設登入頁面。

現在我們證明了運作 nginx 是多麼的簡單,我們可以關閉這個容器。

docker rm -f web 

把 nginx 配置成一個反向代理伺服器

可以通過像下面這樣編輯位于 /etc/nginx/conf.d/default.conf 的配置檔案,把 nginx 配置成一個反向代理伺服器:

server { 

  listen 80; 

  location / { 

    proxy_pass http://localhost:6666; 

  } 

通過上面的配置可以讓 nginx 将所有對根目錄的通路請求代理到 http://localhost:6666。記住這裡的 localhost 指的是運作 nginx 的容器。我們可以在 nginx容器内部利用卷來使用我們自己的配置檔案:

docker run -d -p 8080:80 \ 

-v /path/to/my.conf:/etc/nginx/conf.d/default.conf \ 

nginx 

注意:這把一個單一檔案從主機映射到容器中,而不是一個完整目錄。

在容器間進行通信

docker 允許内部容器通過共享虛拟網絡進行通信。預設情況下,所有通過 docker 守護程序啟動的容器都可以通路一種叫做“橋”的虛拟網絡。這使得一個容器可以被另一個容器在相同的網絡上通過 ip 位址和端口來引用。

你可以通過監測(inspect)容器來找到它的 ip 位址。我将從之前建立的 niksoper/netcore-books 鏡像中啟動一個容器并監測(inspect)它:

docker run -d -p 5000:5000 --name books niksoper/netcore-books 

docker inspect books  

通過Docker在Linux上托管.NET Core

我們可以看到這個容器的 ip 位址是 "ipaddress": "172.17.0.3"。

是以現在如果我建立下面的 nginx 配置檔案,并使用這個檔案啟動一個 nginx 容器, 它将代理請求到我的 api :

    proxy_pass http://172.17.0.3:5000; 

現在我可以使用這個配置檔案啟動一個 nginx 容器(注意我把主機上的 8080 端口映射到了 nginx 容器上的 80 端口):

-v ~/dev/nginx/my.nginx.conf:/etc/nginx/conf.d/default.conf \ 

一個到 http://localhost:8080 的請求将被代理到應用上。注意下面 curl 響應的 server 響應頭:

通過Docker在Linux上托管.NET Core

docker compose

在這個地方,我為自己的進步而感到高興,但我認為一定還有更好的方法來配置 nginx,可以不需要知道應用程式容器的确切 ip 位址。另一個當地的 scott logic devops 大師 jason ebbin 在這個地方進行了改進,并建議使用 docker compose。

概況描述一下,docker compose 使得一組通過聲明式文法互相連接配接的容器很容易啟動。我不想再細說 docker compose 是如何工作的,因為你可以在之前的文章中找到。

我将通過一個我所使用的 docker-compose.yml 檔案來啟動:

version: '2' 

services: 

    books-service: 

        container_name: books-api 

        build: . 

    reverse-proxy: 

        container_name: reverse-proxy 

        image: nginx 

        ports: 

         - "9090:8080" 

        volumes: 

         - ./proxy.conf:/etc/nginx/conf.d/default.conf 

這是版本 2 文法,是以為了能夠正常工作,你至少需要 1.6 版本的 docker compose。

這個檔案告訴 docker 建立兩個服務:一個是給應用的,另一個是給 nginx 反向代理伺服器的。

books-service

這個與 docker-compose.yml 相同目錄下的 dockerfile 建構的容器叫做 books-api。注意這個容器不需要釋出任何端口,因為隻要能夠從反向代理伺服器通路它就可以,而不需要從主機作業系統通路它。

reverse-proxy

這将基于 nginx 鏡像啟動一個叫做 reverse-proxy 的容器,并将位于目前目錄下的 proxy.conf 檔案挂載為配置。它把主機上的 9090 端口映射到容器中的 8080 端口,這将允許我們在 http://localhost:9090 上通過主機通路容器。

proxy.conf 檔案看起來像下面這樣:

    listen 8080; 

    location / { 

      proxy_pass http://books-service:5000; 

    } 

這兒的關鍵點是我們現在可以通過名字引用 books-service,是以我們不需要知道 books-api 這個容器的 ip 位址!

現在我們可以通過一個運作着的反向代理啟動兩個容器(-d 意味着這是獨立的,是以我們不能看到來自容器的輸出):

docker compose up -d 

驗證我們所建立的容器:

docker ps 

最後來驗證我們可以通過反向代理來控制該 api :

curl -i http://localhost:9090/api/books 

怎麼做到的?

docker compose 通過建立一個新的叫做 mvclibrary_default 的虛拟網絡來實作這件事,這個虛拟網絡同時用于 books-api 和 reverse-proxy 容器(名字是基于 docker-compose.yml 檔案的父目錄)。

通過 docker network ls 來驗證網絡已經存在:

通過Docker在Linux上托管.NET Core

你可以使用 docker network inspect mvclibrary_default 來看到新的網絡的細節:

通過Docker在Linux上托管.NET Core

注意 docker 已經給網絡配置設定了子網:"subnet": "172.18.0.0/16"。/16 部分是無類域内路由選擇(cidr),完整的解釋已經超出了本文的範圍,但 cidr 隻是表示 ip 位址範圍。運作 docker network inspect bridge 顯示子網:"subnet": "172.17.0.0/16",是以這兩個網絡是不重疊的。

現在用 docker inspect books-api 來确認應用程式的容器正在使用該網絡:

通過Docker在Linux上托管.NET Core

注意容器的兩個别名("aliases")是容器辨別符(3c42db680459)和由 docker-compose.yml 給出的服務名(books-service)。我們通過 books-service 别名在自定義 nginx 配置檔案中來引用應用程式的容器。這本可以通過 docker network create 手動建立,但是我喜歡用 docker compose,因為它可以幹淨簡潔地将容器建立和依存捆綁在一起。

結論

是以現在我可以通過幾個簡單的步驟在 linux 系統上用 nginx 運作應用程式,不需要對主機作業系統做任何長期的改變:

git checkout blog-docker 

docker-compose up -d 

我知道我在這篇文章中所寫的内容不是一個真正的生産環境就緒的裝置,因為我沒有寫任何有關下面這些的内容,絕大多數下面的這些主題都需要用單獨一篇完整的文章來叙述。

安全考慮比如防火牆和 ssl 配置

如何確定應用程式保持運作狀态

如何選擇需要包含的 docker 鏡像(我把所有的都放入了 dockerfile 中)

資料庫 - 如何在容器中管理它們

對我來說這是一個非常有趣的學習經曆,因為有一段時間我對探索 asp.net core 的跨平台支援非常好奇,使用 “configuratin as code” 的 docker compose 方法來探索一下 devops 的世界也是非常愉快并且很有教育意義的。

如果你對 docker 很好奇,那麼我鼓勵你來嘗試學習它 或許這會讓你離開舒适區,不過,有可能你會喜歡它?