歡迎光臨
每天分享高質量文章

使用 Docker 在 Linux 上託管 ASP.NET Core 應用程式

說在前面

在閱讀本文之前,您必須對 Docker 的中涉及的基本概念以及常見命令有一定瞭解,本文側重實戰,不會對相關概念詳述。

同時請確保您本地開發機器已完成如下安裝:

  • Docker 18.06 或更高版本的 Docker 客戶端
  • .NET Core SDK 2.2 或更高版本
  • Visual Studio Code 程式碼編輯器,以及 C# 語法外掛 1.17.1 或更高版本

註:本文實驗環境是 Ubuntu 18.04 LTS。如果您的機器是 Window,也可以把 Docker 裝在虛擬機器或伺服器上。

建立演示專案

開始之前要先準備一個需要 Docker 容器化的 ASP.NET Core 應用程式,用於下麵的操作演示。這裡我用 .NET Core CLI 快速搭建一個全新的 Web API 專案。

啟動 VS Code,開啟整合終端,輸入如下命令:

Copy

dotnet new webapi -o TodoApi
code -r TodoApi

以上便建立了一個名為TodoApi的 Web API 樣板專案。

開啟整合終端,輸入dotnet run命令編譯執行程式,然後開啟瀏覽器跳轉到 URL http://localhost:5000/api/values,如正常傳回如下 JSON 資料,說明應用程式本地成功執行。

Copy

["value1","value2"]

現在讓我們更進一步,在 Docker 中構建並執行該應用程式。

建立 Dockerfile 檔案

Dockerfile 是一個文字檔案,其用來定義單個容器的內容和啟動行為,按順序包含構建映象所需的所有指令。Docker 會透過讀取 Dockerfile 中的指令自動構建映象。

在專案TodoApi根目錄中,建立一個名為Dockerfile的檔案,並貼上以下內容:

Copy

FROM microsoft/dotnet:2.2-sdk AS build-env
WORKDIR /app

COPY *.csproj ./
RUN dotnet restore

COPY . ./
RUN dotnet publish -c Release -o out

FROM microsoft/dotnet:2.2-aspnetcore-runtime
WORKDIR /app
COPY --from=build-env /app/out .
ENTRYPOINT ["dotnet", "TodoApi.dll"]

  • FROM指令必須放在第一位,用於初始化映象,為後面的指令設定基礎映象。
  • WORKDIR 指令為其他指令設定工作目錄,如果不存在,則會建立該目錄。
  • COPY指令會從源路徑複製新檔案或目錄,並將它們新增到路徑標的容器的檔案系統中。
  • RUN指令可以在當前映象之上的新 層 中執行任何命令並提交結果,生成的已提交映象將用於 Dockerfile 中的下一步。
  • ENTRYPOINT指令支援以可執行檔案的形式執行容器。

有關 Dockerfile 中指令用法的更多資訊請參閱 Dockerfile reference。

同時,為了避免構建專案中的一些除錯生成檔案,可以在專案檔案夾中新增.dockerignore檔案,並貼上如下內容:

Copy

bin\
obj\

構建應用容器映象

在專案TodoApi根目錄中,開啟整合終端,執行如下命令構建容器映象:

Copy

docker build -t todoapi .

-t引數用來指定映象的名字及標簽,通常是name:tag或者name格式。本例todoapi便是我們給映象起的名字,沒有設定標簽即使用預設標簽latest

如命令執行成功,終端會有類似如下輸出:

Copy

$ docker build -t todoapi .
Sending build context to Docker daemon 1.137MB
Step 1/10 : FROM microsoft/dotnet:2.2-sdk AS build-env
2.2-sdk: Pulling from microsoft/dotnet
e79bb959ec00: Pull complete
d4b7902036fe: Pull complete
1b2a72d4e030: Pull complete
d54db43011fd: Pull complete
b3ae1535ac68: Pull complete
f04cf82b07ad: Pull complete
6f91a9d92092: Pull complete
Digest: sha256:c443ff79311dde76cb1acf625ae47581da45aad4fd66f84ab6ebf418016cc008
Status: Downloaded newer image for microsoft/dotnet:2.2-sdk
---> e268893be733
Step 2/10 : WORKDIR /app
---> Running in c7f62130f331
Removing intermediate container c7f62130f331
---> e8b6a73d3d84
Step 3/10 : COPY *.csproj ./
---> cfa03afa6003
Step 4/10 : RUN dotnet restore
---> Running in d96a9b89e4a9
Restore completed in 924.67 ms for /app/TodoApi.csproj.
Removing intermediate container d96a9b89e4a9
---> 14d5d32d40b6
Step 5/10 : COPY . ./
---> b1242ea0b0b8
Step 6/10 : RUN dotnet publish -c Release -o out
---> Running in 37c8eb07c86e
Microsoft (R) Build Engine version 16.0.450+ga8dc7f1d34 for .NET Core
Copyright (C) Microsoft Corporation. All rights reserved.

Restore completed in 663.74 ms for /app/TodoApi.csproj.
TodoApi -> /app/bin/Release/netcoreapp2.2/TodoApi.dll
TodoApi -> /app/out/
Removing intermediate container 37c8eb07c86e
---> 6238f4c1cf07
Step 7/10 : FROM microsoft/dotnet:2.2-aspnetcore-runtime
2.2-aspnetcore-runtime: Pulling from microsoft/dotnet
27833a3ba0a5: Pull complete
25dbf7dc93e5: Pull complete
0ed9cb15d3b8: Pull complete
874ea13b7488: Pull complete
Digest: sha256:ffd756d34bb0f976ba5586f6c88597765405af8014ae51b34811992b46ba40e8
Status: Downloaded newer image for microsoft/dotnet:2.2-aspnetcore-runtime
---> cb2dd04458bc
Step 8/10 : WORKDIR /app
---> Running in b0a3826d346b
Removing intermediate container b0a3826d346b
---> 4218db4cc2f5
Step 9/10 : COPY --from=build-env /app/out .
---> 765168aa2c7a
Step 10/10 : ENTRYPOINT ["dotnet", "TodoApi.dll"]
---> Running in f93bcaf5591f
Removing intermediate container f93bcaf5591f
---> 046226f5e9cb
Successfully built 046226f5e9cb
Successfully tagged todoapi:latest

如果您的機器是第一次構建,速度可能會有些慢,因為要從 Docker Hub 上拉取應用依賴的dotnet-sdkaspnetcore-runtime基礎映象。

構建完成後,我們可以透過docker images命令確認本地映象倉庫是否存在我們構建的映象todoapi

Copy

REPOSITORY TAG IMAGE ID CREATED SIZE
todoapi latest c92a82f0efaa 19 hours ago 260MB
microsoft/dotnet 2.2-sdk 5e09f77009fa 26 hours ago 1.74GB
microsoft/dotnet 2.2-aspnetcore-runtime 08ed21b5758c 26 hours ago 260MB
...

執行應用容器

容器映象構建完成後,就可以使用docker run命令執行容器了,有關該命令引數的更多資訊請參閱 Reference – docker run 。

開發環境下,通常會透過docker run --rm -it命令執行應用容器,具體命令如下:

Copy

docker run --rm -it -p 5000:80 todoapi

  • -it引數表示以互動樣式執行容器併為容器重新分配一個偽輸入終端,方便檢視輸出除錯程式。
  • --rm引數表示將會在容器退出後自動刪除當前容器,開發樣式下常用引數。
  • -p引數表示會將本地計算機上的5000埠對映到容器中的預設80埠,埠對映的關係為host:container
  • todoapi便是我們要啟動的本地映象名稱。

如命令執行成功,終端會有類似如下輸出:

Copy

$ docker run -it --rm -p 5000:80 todoapi
warn: Microsoft.AspNetCore.DataProtection.KeyManagement.XmlKeyManager[35]
No XML encryptor configured. Key {1a78d899-738b-4aea-a7d6-777302933f38} may be persisted to storage in unencrypted form.
Hosting environment: Production
Content root path: /app
Now listening on: http:
Application started. Press Ctrl+C to shut down.

生產環境下,通常會透過docker run -d命令執行應用容器,具體命令如下:

Copy

docker run -d --restart=always --name myapp -p 5000:80 todoapi

  • -d引數表示會將容器作為服務啟動,不需要終端互動。
  • --name引數用來指定容器名稱,本例指定容器名稱為myapp
  • --restart是一個面向生產環境的引數,用來指定容器非正常退出時的重啟策略,本例always表示始終重新啟動容器,其他可選策略請參考 Restart policies (–restart)。

如命令執行成功,終端會有類似如下輸出:

Copy

$ docker run -d --restart=always --name myapp -p 5000:80 todoapi
e3d747d9d2b4cd14b2acb24f81bea9312f89c4eb689dba5f6559950c91db1600

容器啟動後,在 Web 瀏覽器中再次訪問http://localhost:5000/api/values,應該會和本地測試一樣傳回如下 JSON 資料:

Copy

["value1","value2"]

至此,我們的 ASP.NET Core 應用就成功執行在 Docker 容器中了。

多容器應用部署

目前我們建立的演示專案TodoApi過於簡單,真實的生產專案肯定會涉及更多其他的依賴。例如:關係資料庫 Mysql、檔案資料庫 MongoDB、分散式快取 Redis、訊息佇列 RabbitMQ 等各種服務。

還有就是,生產環境我們一般不會將 ASP.NET Core 應用程式的宿主伺服器 Kestrel 直接暴露給使用者,通常是在前面加一個反向代理服務 Nginx。

這些依賴服務還要像傳統部署方式那樣,一個一個單獨配置部署嗎?不用的,因為它們本身也是可以被容器化的,所以我們只要考慮如何把各個相互依賴的容器聯絡到一起,這就涉及到容器編排,而 Docker Compose 正是用來解決這一問題的,最終可以實現多容器應用的一鍵部署。

Docker Compose 是一個用於定義和執行多容器的 Docker 工具。其使用YAML檔案來配置應用程式的服務,最終您只要使用一個命令就可以從配置中建立並啟動所有服務。

安裝 Docker Compose

Linux 系統下的安裝過程大致分為以下幾步:

Step1:執行如下命令下載 Compose 最新穩定版本,截止發稿前最新版本為1.24.0

Copy

sudo curl -L "https://github.com/docker/compose/releases/download/1.24.0/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

Step2:對下載完成的二進製程式新增可執行許可權。

Copy

sudo chmod +x /usr/local/bin/docker-compose

Step3:測試安裝是否成功。

Copy

$ docker-compose --version
docker-compose version 1.24.0, build 0aa59064

若您在安裝過程中遇到問題,或是其他系統安裝請參閱 Install Docker Compose。

改造演示專案

現在來改造一下我們的演示專案TodoApi,新增 Redis 分散式快取、使用 Nginx 做反向代理,準備構建一個具如下圖所示架構的多容器應用。

TodoApi專案根目錄下,開啟整合終端,輸入如下命令新增 Redis 依賴包。

Copy

dotnet add package Microsoft.Extensions.Caching.StackExchangeRedis --version 2.2.0

修改應用啟動配置檔案Startup.cs中的ConfigureServices方法:

Copy


public void ConfigureServices(IServiceCollection services)
{
services.AddStackExchangeRedisCache(options =>
{
options.Configuration = Configuration.GetConnectionString("Redis");
});

services.AddHttpContextAccessor();

services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}

TodoApi專案Controllers目錄下新建控制器HelloController,具體程式碼如下所示:

Copy

using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Distributed;

namespace TodoApi.Controllers
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Caching.Distributed;

namespace TodoApi.Controllers
{
[Route("api/[controller]")]
[ApiController]
public class HelloController : ControllerBase
{
private readonly IDistributedCache _distributedCache;
private readonly IHttpContextAccessor _httpContextAccessor;

public HelloController(
IDistributedCache distributedCache,
IHttpContextAccessor httpContextAccessor
)
{
_distributedCache = distributedCache;
_httpContextAccessor = httpContextAccessor;
}

[HttpGet]
public ActionResult<string> Get()
{
var connection = _httpContextAccessor.HttpContext.Connection;
var ipv4 = connection.LocalIpAddress.MapToIPv4().ToString();
var message = $"Hello from Docker Container:{ipv4}";

return message;
}

[HttpGet("{name}")]
public ActionResult<string> Get(string name)
{
var defaultKey = $"hello:{name}";
_distributedCache.SetString(defaultKey, $"Hello {name} form Redis");
var message = _distributedCache.GetString(defaultKey);

return message;
}
}
}

以上控制器,提供了兩個介面/api/hello/api/hello/{name},分別用來測試 Nginx 負載均衡和 Redis 的聯通性。

建立 docker-compose.yml

準備工作就緒,下麵我們就可以使用 Docker Compose 來編排容器。

同樣是在TodoApi專案根目錄中,建立一個名為docker-compose.yml的檔案,並貼上以下內容:

Copy

version: "3.7"
services:
myproject-todoapi-1:
container_name: my-todoapi-1
build:
context: .
dockerfile: Dockerfile
restart: always
ports:
- "5001:80"
volumes:
- ./appsettings.json:/app/appsettings.json

myproject-todoapi-2:
container_name: my-todoapi-2
build:
context: .
dockerfile: Dockerfile
restart: always
ports:
- "5002:80"
volumes:
- ./appsettings.json:/app/appsettings.json

myproject-todoapi-3:
container_name: my-todoapi-3
build:
context: .
dockerfile: Dockerfile
restart: always
ports:
- "5003:80"
volumes:
- ./appsettings.json:/app/appsettings.json

myproject-nginx:
container_name: my-nginx
image: nginx
restart: always
ports:
- "80:80"
volumes:
- ./conf/nginx.conf:/etc/nginx/conf.d/default.conf

myproject-redis:
container_name: my-redis
image: redis
restart: always
ports:
- "6379:80"
volumes:
- ./conf/redis.conf:/etc/redis/redis.conf

其中version 用來指定 Compose 檔案版本號,3.7是目前最新版本,具體哪些版本對應哪些特定的 Docker 引擎版本請參閱 Compose file versions and upgrading。

Compose 中強化了服務的概念,簡單地理解就是, 服務是一種用於生產環境的容器。一個多容器 Docker 應用由若干個服務組成,如上檔案即定義了 5 個服務

  • 3 個應用服務myproject-todoapi-1myproject-todoapi-2myproject-todoapi-3
  • 1 個 Nginx 服務myproject-reverse-proxy
  • 1 個 Redis 服務myproject-redis

以上 5 個服務的配置引數相差無幾、也很簡單,我就不展開敘述,不清楚的可以參閱 Compose file reference。

這裡只講一個配置引數volumes

我們知道,容器中的檔案在宿主機上存在形式複雜,修改檔案需要先透過如下命令進入容器後操作。

Copy

docker exec -it <CONTAINER ID/NAMES> /bin/bash

容器一旦刪除,其內部配置以及產生的資料也會丟失。

為瞭解決這些問題,Docker 引入了資料捲 volumes 機制。即 Compose 中 volumes 引數用來將宿主機的某個目錄或檔案對映掛載到 Docker 容器內部的對應的目錄或檔案,通常被用來靈活掛載配置檔案或持久化容器產生的資料。

建立相關配置檔案

接下來,需要根據如上docker-compose.yml檔案中涉及的volumes配置建立三個配置檔案。要知道,它們最終是需要被註入到 Docker 容器中的

首先,在TodoApi專案根目錄中,建立三個應用服務myproject-todoapi-*需要的程式配置檔案appsettings.json,具體內容如下:

Copy

"ConnectionStrings": {
"Redis": "myproject-redis:6379,password=todoapi@2019"
},

以上配置,指定了 Redis 服務myproject-redis的連線字串,其中myproject-redis可以看到是 Redis 服務的服務名稱,當該配置檔案註入到 Docker 容器中後,會自動解析為容器內部 IP,同時考慮到 Redis 服務的安全性,為其指定了密碼,即password=todoapi@2019

然後,在TodoApi專案根目錄中建立一個子目錄conf,用來存放 Nginx 和 Redis 的配置檔案。

Copy

mkdir conf && cd conf

先來建立 Redis 服務myproject-redis的配置檔案。

可以透過如下命令,下載一個 Redis 官方提供的標準配置檔案redis.conf

Copy

wget http:

然後開啟下載後的redis.conf檔案,找到SECURITY節點,根據如上應用服務的 Redis 連線字串資訊,啟用並改下密碼:

Copy

requirepass todoapi@2019

再來建立 Nginx 服務myproject-nginx的配置檔案。

conf目錄中,建立一個名為nginx.conf的配置檔案,並貼上如下內容:

Copy

upstream todoapi {
server myproject-todoapi-1:80;
server myproject-todoapi-2:80;
server myproject-todoapi-3:80;
}
server {
listen 80;
location / {
proxy_pass http://todoapi;
proxy_set_essay-header Host $host;
proxy_set_essay-header X-Real-IP $remote_addr;
proxy_set_essay-header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}

以上配置,是一個 Nginx 中具備負載均衡的代理配置,其預設採用輪循策略將請求轉發給 Docker 服務myproject-todoapi-1myproject-todoapi-2myproject-todoapi-3

執行並測試多容器應用

經過以上幾個小節,容器編排的過程就完成了,接下來就可以直接定義並啟動我們建立的多容器應用實體了。

切換到docker-compose.yml檔案所在的目錄,也就是TodoApi專案的根目錄,執行如下命令:

Copy

docker-compose up -d

如命令執行成功,終端最後會有類似如下輸出:

Copy

......
Creating my-todoapi-1 ... done
Creating my-redis ... done
Creating my-todoapi-3 ... done
Creating my-nginx ... done
Creating my-todoapi-2 ... done

至此,我們的多容器應用就已經在運行了,可以透過docker-compose ps命令來確認下。

Copy

$ docker-compose ps
Name Command State Ports

my-nginx nginx -g daemon off; Up 0.0.0.0:80->80/tcp
my-redis docker-entrypoint.sh redis ... Up 6379/tcp, 0.0.0.0:6379->80/tcp
my-todoapi-1 dotnet TodoApi.dll Up 0.0.0.0:5001->80/tcp
my-todoapi-2 dotnet TodoApi.dll Up 0.0.0.0:5002->80/tcp
my-todoapi-3 dotnet TodoApi.dll Up 0.0.0.0:5003->80/tcp

可以透過連續三次請求/api/hello介面測試應用的負載均衡。

Copy

curl http:
curl http:
curl http:

Hello from Docker Container:172.30.0.2
Hello from Docker Container:172.30.0.4
Hello from Docker Container:172.30.0.5

三個應用服務分別部署在不同容器中,所以理論上來講,他們的容器內部 IP 也是不同的,所以/api/hello介面每次輸出資訊不會相同。

請求/api/hello/{name}介面測試 Redis 服務連通性。

Copy

curl http:

Hello esofar form Redis

小結

本文從零構建了一個 ASP.NET Core 應用,並透過 Docker 部署,然後由淺入深,引入 Docker Compose 演示了多容器應用的部署過程。透過本文的實戰您可以更深入地瞭解 Docker。本文涉及的程式碼已託管到以下地址,您在實驗過程中遇到問題可以參考。

https://github.com/esofar/dockerize-aspnetcore-samples

Docker 命令附錄

Copy

$ docker --help

Usage: docker [OPTIONS] COMMAND

A self-sufficient runtime for containers

Options:
--config string Location of client config files (default "/root/.docker")
-D, --debug Enable debug mode
-H, --host list Daemon socket(s) to connect to
-l, --log-level string Set the logging level ("debug"|"info"|"warn"|"error"|"fatal") (default "info")
--tls Use TLS; implied by --tlsverify
--tlscacert string Trust certs signed only by this CA (default "/root/.docker/ca.pem")
--tlscert string Path to TLS certificate file (default "/root/.docker/cert.pem")
--tlskey string Path to TLS key file (default "/root/.docker/key.pem")
--tlsverify Use TLS and verify the remote
-v, --version Print version information and quit

Management Commands:
builder Manage builds
config Manage Docker configs
container Manage containers
engine Manage the docker engine
image Manage images
network Manage networks
node Manage Swarm nodes
plugin Manage plugins
secret Manage Docker secrets
service Manage services
stack Manage Docker stacks
swarm Manage Swarm
system Manage Docker
trust Manage trust on Docker images
volume Manage volumes

Commands:
attach Attach local standard input, output, and error streams to a running container
build Build an image from a Dockerfile
commit Create a new image from a container
cp Copy files/folders between a container and the local filesystem
create Create a new container
diff Inspect changes to files or directories on a container
events Get real time events from the server
exec Run a command in a running container
export Export a container
history Show the history of an image
images List images
import Import the contents from a tarball to create a filesystem image
info Display system-wide information
inspect Return low-level information on Docker objects
kill Kill one or more running containers
load Load an image from a tar archive or STDIN
login Log in to a Docker registry
logout Log out from a Docker registry
logs Fetch the logs of a container
pause Pause all processes within one or more containers
port List port mappings or a specific mapping for the container
ps List containers
pull Pull an image or a repository from a registry
push Push an image or a repository to a registry
rename Rename a container
restart Restart one or more containers
rm Remove one or more containers
rmi Remove one or more images
run Run a command in a new container
save Save one or more images to a tar archive (streamed to STDOUT by default)
search Search the Docker Hub for images
start Start one or more stopped containers
stats Display a live stream of container(s) resource usage statistics
stop Stop one or more running containers
tag Create a tag TARGET_IMAGE that refers to SOURCE_IMAGE
top Display the running processes of a container
unpause Unpause all processes within one or more containers
update Update configuration of one or more containers
version Show the Docker version information
wait Block until one or more containers stop, then print their exit codes

Docker Compose 命令附錄

Copy

$ docker-compose
Define and run multi-container applications with Docker.

Usage:
docker-compose [-f ...] [options] [COMMAND] [ARGS...]
docker-compose -h|

Options:
-f,
(default: docker-compose.yml)
-p,
(default: directory name)

-v,
-H,

name specified in the client certificate

(default: the path of the Compose file)

in v3 files to their non-Swarm equivalent

Commands:
build Build or rebuild services
bundle Generate a Docker bundle from the Compose file
config Validate and view the Compose file
create Create services
down Stop and remove containers, networks, images, and volumes
events Receive real time events from containers
exec Execute a command in a running container
help Get help on a command
images List images
kill Kill containers
logs View output from containers
pause Pause services
port Print the public port for a port binding
ps List containers
pull Pull service images
push Push service images
restart Restart services
rm Remove stopped containers
run Run a one-off command
scale Set number of containers for a service
start Start services
stop Stop services
top Display the running processes
unpause Unpause services
up Create and start containers
version Show the Docker-Compose version information

相關閱讀

  • ASP.NET Core Docker Sample
  • Dockerize a .NET Core application
  • Best practices for writing Dockerfiles
  • Fun With Docker-Compose Using .NET Core And NGINX
  • ASP.NET Core 2.2: implementando Load Balancing com Nginx, Docker e Docker Compose

已同步到看一看
贊(0)

分享創造快樂