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

說在前面

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

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

注:本文實驗環境是 Ubuntu 18.04 LTS。若是您的機器是 Window,也能夠把 Docker 裝在虛擬機或服務器上。git

建立演示項目

開始以前要先準備一個須要 Docker 容器化的 ASP.NET Core 應用程序,用於下面的操做演示。這裏我用 .NET Core CLI 快速搭建一個全新的 Web API 項目。github

啓動 VS Code,打開集成終端,輸入以下命令:web

dotnet new webapi -o TodoApi
code -r TodoApi

以上便建立了一個名爲TodoApi的 Web API 樣板項目。redis

打開集成終端,輸入dotnet run命令編譯運行程序,而後打開瀏覽器跳轉到 URL http://localhost:5000/api/values,如正常返回以下 JSON 數據,說明應用程序本地成功運行。sql

["value1","value2"]

如今讓咱們更進一步,在 Docker 中構建並運行該應用程序。docker

建立 Dockerfile 文件

Dockerfile 是一個文本文件,其用來定義單個容器的內容和啓動行爲,按順序包含構建鏡像所需的全部指令。Docker 會經過讀取 Dockerfile 中的指令自動構建鏡像。數據庫

在項目TodoApi根目錄中,建立一個名爲Dockerfile的文件,並粘貼如下內容:json

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

# Copy csproj and restore as distinct layers
COPY *.csproj ./
RUN dotnet restore

# Copy everything else and build
COPY . ./
RUN dotnet publish -c Release -o out

# Build runtime image
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文件,並粘貼以下內容:

bin\
obj\

構建應用容器鏡像

在項目TodoApi根目錄中,打開集成終端,執行以下命令構建容器鏡像:

docker build -t todoapi .

-t參數用來指定鏡像的名字及標籤,一般是name:tag或者name格式。本例todoapi即是咱們給鏡像起的名字,沒有設置標籤即便用默認標籤latest

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

$ 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

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命令運行應用容器,具體命令以下:

docker run --rm -it -p 5000:80 todoapi
  • -it參數表示以交互模式運行容器併爲容器從新分配一個僞輸入終端,方便查看輸出調試程序。
  • --rm參數表示將會在容器退出後自動刪除當前容器,開發模式下經常使用參數。
  • -p參數表示會將本地計算機上的5000端口映射到容器中的默認80端口,端口映射的關係爲host:container
  • todoapi即是咱們要啓動的本地鏡像名稱。

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

$ 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://[::]:80
Application started. Press Ctrl+C to shut down.

生產環境下,一般會經過docker run -d命令運行應用容器,具體命令以下:

docker run -d --restart=always --name myapp -p 5000:80 todoapi
  • -d參數表示會將容器做爲服務啓動,不須要終端交互。
  • --name參數用來指定容器名稱,本例指定容器名稱爲myapp
  • --restart是一個面向生產環境的參數,用來指定容器非正常退出時的重啓策略,本例always表示始終從新啓動容器,其餘可選策略請參考 Restart policies (--restart)

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

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

容器啓動後,在 Web 瀏覽器中再次訪問http://localhost:5000/api/values,應該會和本地測試同樣返回以下 JSON 數據:

["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

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:對下載完成的二進制程序添加可執行權限。

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

Step3:測試安裝是否成功。

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

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

改造演示項目

如今來改造一下咱們的演示項目TodoApi,添加 Redis 分佈式緩存、使用 Nginx 作反向代理,準備構建一個具以下圖所示架構的多容器應用。

TodoApi項目根目錄下,打開集成終端,輸入以下命令新增 Redis 依賴包。

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

修改應用啓動配置文件Startup.cs中的ConfigureServices方法:

// This method gets called by the runtime. Use this method to add services to the container.
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,具體代碼以下所示:

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的文件,並粘貼如下內容:

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

咱們知道,容器中的文件在宿主機上存在形式複雜,修改文件須要先經過以下命令進入容器後操做。

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

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

爲了解決這些問題,Docker 引入了數據卷 volumes 機制。即 Compose 中 volumes 參數用來將宿主機的某個目錄或文件映射掛載到 Docker 容器內部的對應的目錄或文件,一般被用來靈活掛載配置文件或持久化容器產生的數據。

PS:本身動手編寫docker-compose.yml的時候,能夠嘗試實驗更多場景。好比:新增一個 MySQL 依賴服務、把容器內產生的數據持久化到宿主機等等。

建立相關配置文件

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

首先,在TodoApi項目根目錄中,建立三個應用服務myproject-todoapi-*須要的程序配置文件appsettings.json,具體內容以下:

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

以上配置,指定了 Redis 服務myproject-redis的鏈接字符串,其中myproject-redis能夠看到是 Redis 服務的服務名稱,當該配置文件注入到 Docker 容器中後,會自動解析爲容器內部 IP,同時考慮到 Redis 服務的安全性,爲其指定了密碼,即password=todoapi@2019

而後,在TodoApi項目根目錄中建立一個子目錄conf,用來存放 Nginx 和 Redis 的配置文件。

mkdir conf && cd conf

先來建立 Redis 服務myproject-redis的配置文件。

能夠經過以下命令,下載一個 Redis 官方提供的標準配置文件redis.conf

wget http://download.redis.io/redis-stable/redis.conf

而後打開下載後的redis.conf文件,找到SECURITY節點,根據如上應用服務的 Redis 鏈接字符串信息,啓用並改下密碼:

requirepass todoapi@2019

再來建立 Nginx 服務myproject-nginx的配置文件。

conf目錄中,建立一個名爲nginx.conf的配置文件,並粘貼以下內容:

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_header Host $host;
        proxy_set_header X-Real-IP $remote_addr;
        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
    }
}

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

運行並測試多容器應用

通過以上幾個小節,容器編排的過程就完成了,接下來就能夠直接定義並啓動咱們建立的多容器應用實例了。

切換到docker-compose.yml文件所在的目錄,也就是TodoApi項目的根目錄,執行以下命令:

docker-compose up -d

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

......
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命令來確認下。

$ 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接口測試應用的負載均衡。

curl http://localhost/api/hello
curl http://localhost/api/hello
curl http://localhost/api/hello

// Output:

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 服務連通性。

curl http://localhost/api/hello/esofar

// Output:

Hello esofar form Redis

小結

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

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

Docker 命令附錄

$ 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's changes
  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's filesystem
  events      Get real time events from the server
  exec        Run a command in a running container
  export      Export a container's filesystem as a tar archive
  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 命令附錄

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

Usage:
  docker-compose [-f <arg>...] [options] [COMMAND] [ARGS...]
  docker-compose -h|--help

Options:
  -f, --file FILE             Specify an alternate compose file
                              (default: docker-compose.yml)
  -p, --project-name NAME     Specify an alternate project name
                              (default: directory name)
  --verbose                   Show more output
  --log-level LEVEL           Set log level (DEBUG, INFO, WARNING, ERROR, CRITICAL)
  --no-ansi                   Do not print ANSI control characters
  -v, --version               Print version and exit
  -H, --host HOST             Daemon socket to connect to

  --tls                       Use TLS; implied by --tlsverify
  --tlscacert CA_PATH         Trust certs signed only by this CA
  --tlscert CLIENT_CERT_PATH  Path to TLS certificate file
  --tlskey TLS_KEY_PATH       Path to TLS key file
  --tlsverify                 Use TLS and verify the remote
  --skip-hostname-check       Don't check the daemon's hostname against the
                              name specified in the client certificate
  --project-directory PATH    Specify an alternate working directory
                              (default: the path of the Compose file)
  --compatibility             If set, Compose will attempt to convert keys
                              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

相關閱讀

相關文章
相關標籤/搜索