.NET Core和Docker的结合使用

据说容器技术是我们这个行业的一个重要趋势,而博主恰好在近期遇到了这样的需求。

参考MSDN博客,我们先来看看开发人员迁移到容器的关键原因:

  • 一致性:容器包含应用程序及其所有依赖项。不管是在计算机、本地环境或是云端,应用程序都执行相同的代码。

  • 轻量级:通过使用基于主机操作系统的最小量级抽象,并在容器之间共享公共资源,容器可以快速启动并实现最少的内存占用。

  • 共享性:容器镜像可以通过 Docker Hub、Docker Store 和私有 Docker 仓库(如 Azure 镜像仓库)轻松实现分享。

  • 简单而强大:DockerFile 格式(容器镜像的奥秘所在)是一种可以实现强大场景的简单格式:优雅地将操作系统和容器的特定命令结合,并且还可以创建 Docker 镜像层。

接下来,我们看看具体如何在 Docker 中部署 .NET Core 应用。
注:本篇博客中操作系统使用 CentOS 7

1. 前期准备
  • .NET Core 工程
    本篇博客中新建一个 ASP.NET Core 工程作为示例
    VS 2017 Version 15.7.4
    .NET Core Runtime 2.1
    image
    image

  • Linux 切换至 root 身份(方便后续所有需要相关权限的操作)

$ su -

image

2. 安装 Docker

详情请参考文档:https://docs.docker.com/install/linux/docker-ce/centos/

$ yum install -y docker

$ systemctl start docker

image

注:
CentOS 7 安装好 Docker ,执行启动命令systemctl start docker时,可能出现如下报错:
Error starting daemon: SELinux is not supported with the overlay2 graph driver on this kernel. Either boot into a newer kernel or disable selinux in docker (--selinux-enabled=false)

重新编辑 Docker 配置文件即可(设置 OPTIONS 的--selinux-enabled=false或直接删除--selinux-enabled):

$ vi /etc/sysconfig/docker
# /etc/sysconfig/docker

# Modify these options if you want to change the way the docker daemon runs

OPTIONS='--selinux-enabled=false  --log-driver=journald --signature-verification=false'  
if [ -z "${DOCKER_CERT_PATH}" ]; then  
    DOCKER_CERT_PATH=/etc/docker
fi

:wq

$ systemctl restart docker 

image

3. 确认文件系统

确保 /etc/sysconfig/docker-storage 中 DOCKERSTORAGEOPTIONS="--storage-driver overlay"

注:Docker默认只能识别overlay文件系统,若使用overlay2文件系统,运行Docker镜像时守护进程会报错:
Error response from daemon: error creating overlay mount to /var/lib/docker/overlay2/2c320fe7df5a0e659466c528063c640402e504bc4d1500b78d6a5142707b1487/merged: invalid argument.

$ systemctl stop docker #停掉docker服务

$ rm -rf /var/lib/docker #特别注意:已拉取的docker镜像会被清空

$ vi /etc/sysconfig/docker-storage #设置DOCKER_STORAGE_OPTIONS="--storage-driver overlay"

$ vi /etc/sysconfig/docker #去掉option后面的--selinux-enabled,或将其值置为false(操作同安装Docker中的说明)

$ systemctl start docker #再次启动docker服务

image

4. 修改 /etc/docker/daemon.json 文件来配置镜像加速(考虑到网络防火长城)
$ vi /etc/docker/daemon.json
{
    "registry-mirrors": ["https://registry.docker-cn.com"]
}

:wq

注:请保证 /etc/docker/daemon.json 访问权限

# 授权单个文件
$ chmod 777 /etc/docker/daemon.json

# 递归授权整个文件夹
$ chmod -R 777 /var/lib/docker

image
image

5. 拉取或导入microsoft/dotnet镜像

镜像类别:

  • microsoft/dotnet or microsoft/dotnet:latest (alias for the SDK image)
  • microsoft/dotnet:sdk
  • microsoft/dotnet:runtime
  • microsoft/dotnet:runtime-deps

详情请参见:https://docs.microsoft.com/en-us/dotnet/core/docker/building-net-docker-images

拉取镜像(以镜像 microsoft/dotnet:2.1-aspnetcore-runtime 为例):

$ docker pull microsoft/dotnet:2.1-aspnetcore-runtime

image

*注:*
*A. dotnet sdk的镜像包括了runtime与 dotnet 命令(CLI,即Command Line Interface),体积较大(1.72GB),而我们的程序想要正常运行,使用runtime的镜像(255MB)就足够了*
*B. 本篇博客将以 asp.net core 的工程为例进行后续演示,且写作博文时 dotnet core runtime 版本为2.1,故使用镜像 microsoft/dotnet:2.1-aspnetcore-runtime*
image

导入导出镜像:

#导出镜像(注:625b44243fbe为需要导出的镜像的ID)
$ docker save 625b44243fbe > /home/lary/Documents/dotnetimage.tar

#导入镜像
$ docker load < /home/lary/dotnetimage.tar

#修改导入镜像的名称与标签(注:所导入的镜像名称、标签均为none,625b44243fbe为导入的镜像的ID)
$ docker tag 625b44243fbe microsoft/dotnet:2.1-aspnetcore-runtime
6. 将程序发布内容传输至 CentOS
  • 发布程序 image
    image
    image
  • 文件传输至 CentOS
    image
    注:我将发布后的文件统统扔到 /home/lary/Projects/Lary.Test.Apis 目录下,以便后续操作
7. 创建 Docker 镜像 [可选]

1) 编写适当的 Dockerfile
A. Dockerfile 实例

FROM microsoft/dotnet:2.1-aspnetcore-runtime  
MAINTAINER Lary Mao <dev@lary.me>

LABEL build-date="2018-06-23"

ENV ASPNETCORE_URLS http://0.0.0.0:8023  
WORKDIR /app

COPY * /app/

#也可以通过CMD命令启动脚本文件,并在脚本文件中写入dotnet启动命令
ENTRYPOINT ["dotnet", "Lary.Test.Apis.dll"]  

注:
a. 请注意 Dockerfile 文件名不能有误
b. 我选择将 Dockerfile 放置到程序目录下。各位请根据实际情况修改 Dockerfile 中所涉及到的文件路径(如 COPY 命令)
image

B. 详细说明
参考:https://www.cnblogs.com/sorex/p/6481407.html
DockerFile分为四部分组成:基础镜像信、维护者信息、镜像操作指令和容器启动时执行指令。

  • FROM
    第一行必须指明基于的基础镜像
    例:FROM microsoft/dotnet:2.1-aspnetcore-runtime
  • MAINTAINER
    指定维护者的信息
    例:MAINTAINER Lary Mao dev@lary.me
  • RUN
    格式为Run或者Run [“executable” ,”Param1”, “param2”]
    前者在shell终端上运行,即/bin/sh -C,后者使用exec运行。例如:RUN [“/bin/bash”, “-c”,”echo hello”]
    每条run指令在当前基础镜像执行,并且提交新镜像。当命令比较长时,可以使用“/”换行
  • CMD
    构建容器后调用,也就是在容器启动时才进行调用。
    支持三种格式:
    CMD [“executable” ,”Param1”, “param2”]使用exec执行,推荐
    CMD command param1 param2,在/bin/sh上执行
    CMD [“Param1”, “param2”]提供给ENTRYPOINT做默认参数
  • EXPOSE
    告诉Docker服务端容器暴露的端口号,供互联系统使用。在启动Docker时,可以通过-P,主机会自动分配一个端口号转发到指定的端口。使用-P,则可以具体指定哪个本地端口映射过来
    例:EXPOSE 22 80 8023
  • ENV
    指定一个环境变量,会被后续 RUN 指令使用,并在容器运行时保持
  • ADD
    该命令将指定的文件添加到容器中。其中,源可以是Dockerfile所在目录的一个相对路径;也可以是一个URL;还可以是一个tar文件(自动解压至目录)
  • COPY
    将构建命令所在的主机本地的文件或目录(注意源路径使用相对目录),复制到镜像文件系统(不会自动解压)
  • ENTRYPOINT
    配置容器,使其可执行化
    每个Dockerfile中只能有一个 ENTRYPOINT ,当指定多个时,只有最后一个起效
    例:ENTRYPOINT [“executable”, “param1”, “param2”]
    或:ENTRYPOINT command param1 param2 (shell中执行)
  • VOLUME
    用于指定持久化目录,在容器启动时用-v传递参数,例如-v ~/opt/data/mysql:/var/lib/mysql将本机的~/opt/data/mysql和容器内的/var/lib/mysql做持久化关联
    容器启动时会加载,容器关闭后会回写
    例:VOLUME [“/data”]
  • USER
    指定运行容器时的用户名或UID,后续的 RUN 也会使用指定用户
    当服务不需要管理员权限时,可以通过该命令指定运行用户。并且可以在之前创建所需要的用户,例如:RUN groupadd -r postgres && useradd -r -g postgres postgres。要临时获取管理员权限可以使用 gosu ,而不推荐 sudo
    例:USER daemon
  • WORKDIR
    为后续的 RUN 、 CMD 、 ENTRYPOINT 指令配置工作目录。
    可以使用多个 WORKDIR 指令,后续命令如果参数是相对路径,则会基于之前命令指定的路径。例如:
    WORKDIR /a
    WORKDIR b
    WORKDIR c
    RUN pwd
    则最终路径为 /a/b/c
    例:WORKDIR /path/to/workdir
  • ONBUILD
    配置当所创建的镜像作为其它新创建镜像的基础镜像时,所执行的操作指令。
    例如,Dockerfile使用如下的内容创建了镜像 image-A 。
    […]
    ONBUILD ADD . /app/src
    ONBUILD RUN /usr/local/bin/python-build –dir /app/src
    […]
    如果基于A创建新的镜像时,新的Dockerfile中使用 FROM image-A 指定基础镜像时,会自动执行 ONBUILD 指令内容,等价于在后面添加了两条指令
    例:ONBUILD [INSTRUCTION]

2) 通过命令创建 Docker 镜像

#授权,防止Docker守护进程访问文件权限不够
$ chmod -R 777 /home/lary/Projects/Lary.Test.Apis

#进入到Dockerfile目录
$ cd /home/lary/Projects/Lary.Test.Apis

#创建Docker镜像
$ docker build -t lary/apistest:runtime .

注:lary/apistest 是我为新镜像起的名字,runtime 是新镜像的标签(容器名要求全小写)。此外,请额外注意不要遗漏命令行末尾的那个点。
image

8. 创建 Docker 容器

通过docker run 命令创建新容器(本篇博客中使用端口 8022)

1) 若未按照步骤 7 创建 Docker 镜像
docker run 命令中写入所需的相关信息(如程序工作目录、端口映射等,详情请参见与此步骤同级的详细说明↓)

$ docker run -w /app -e ASPNETCORE_URLS="http://0.0.0.0:8022" --entrypoint "dotnet" --name dotnet-test -h dotnet-test -p 8022:8022 -v /home/lary/Projects/Lary.Test.Apis:/app --privileged=true -d docker.io/microsoft/dotnet:2.1-aspnetcore-runtime Lary.Test.Apis.dll

注:命令中 -w 用于指定程序工作目录(WORKDIR),-e 用于指定 环境变量(ENV), --entrypoint用于指定程序入口,其参数传递必须放置在创建容器所基于的镜像名之后,使用空格进行分隔(Lary.Test.Apis.dll 作为参数传递给 donet 命令)
参考:https://medium.com/@oprearocks/how-to-properly-override-the-entrypoint-using-docker-run-2e081e5feb9d

image

2) 若已按照步骤 7 创建 Docker 镜像
直接根据新创建的镜像创建容器即可

docker run --name dotnet-test -h dotnet-test -p 8023:8022 --privileged=true -d lary/apistest:runtime  

image

3) 详细说明

  • -a stdin
    指定标准输入输出内容类型,可选 STDIN/STDOUT/STDERR 三项
  • -d
    后台运行容器,并返回容器ID
  • -e key="value"
    设置环境变量
  • -h "host"
    指定容器的hostname
  • -i
    以交互模式运行容器,通常与 -t 同时使用
  • -m
    设置容器使用内存最大值
  • -p
    端口映射,使用方法为 主机上的端口:容器内部的端口
  • -t
    为容器重新分配一个伪输入终端,通常与 -i 同时使用
  • -v
    挂载数据卷,使用方法为 数据卷 -v 容器目录 或 -v 本地目录:容器目录
  • --add-host
    为容器添加host与ip的映射关系
  • --cpuset="0-2" or --cpuset="0,1,2"
    绑定容器到指定CPU运行
  • --dns 8.8.8.8
    指定容器使用的DNS服务器,默认和宿主一致
  • --dns-search example.com
    指定容器DNS搜索域名,默认和宿主一致
  • --env-file=[]
    从指定文件读入环境变量
  • --expose=[]
    开放一个端口或一组端口
  • --link=[]
    添加链接到另一个容器
  • --name="nginx-lb"
    为容器指定一个名称
  • --net="bridge"
    指定容器的网络连接类型,支持 bridge/host/none/container四种类型
  • --privileged=true
    使用该参数,container 内的 root 拥有真正的 root 权限。否则, container 内的 root 只是外部的一个普通用户权限。privileged 启动的容器,可以看到很多 host 上的设备,并且可以执行 mount 。甚至允许你在 Docker 容器中启动 Docker 容器。
9. 确保防火墙端口开启
#打开端口/端口段(--permanent永久生效,没有此参数重启后失效)
$ firewall-cmd --zone=public --add-port=8022/tcp --permanent 
$ firewall-cmd --zone=public --add-port=8022-8022/tcp --permanent 

#重新载入
$ firewall-cmd --reload

#查看端口
$ firewall-cmd --zone=public --query-port=8022/tcp

#删除端口
$ firewall-cmd --zone=public --remove-port=8022/tcp --permanent

image

10. 验证

image