Docker容器说到底还是为应用服务的,其最根本的作用还是用来部署我们开发的应用。在开发容器化的应用时,通常都会要创建自定义的镜像。目前在Docker中创建镜像最常用的方法应该就是使用Dockerfile了。Dockerfile是文本文件,一种类似脚本的描述文件,来定义镜像的每层是如何构建,正文部分会详细介绍。
简单概括,开发基于Docker容器的应用程序的常用工作流程:
- 开发环境中完成应用的开发
- 编写Dockerfile文件
- 使用Dockerfile创建自己的镜像
- 使用镜像实例化容器并正常运行
- 测试与修正
- 可以将镜像Push到Hub
本次的学习任务是构建自定义镜像,并将一个ASP.NET Core应用程序使用容器化的方式部署到Linux服务器中。
Dockerfile指令
Docker可以通过定义好的Dockerfile中的指令自动构建映像。虽然现在通过一些高级的IDE(比如visual studio或者visual studio code)可以自动根据当前项目生成Dockerfile,但是了解Dockerfile仍是很有必要的。
为了对Dockerfile有个整体印象,先预览一份官方文档中Dockefile的示例。可以看出来这里就是按步骤定义指令,大小写不敏感但建议全部大写,“#”标识注释文本。
FROM mcr.microsoft.com/dotnet/core/sdk:3.1 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 mcr.microsoft.com/dotnet/core/aspnet:3.1 WORKDIR /app COPY --from=build-env /app/out . ENTRYPOINT ["dotnet", "aspnetapp.dll"]
接下来对Dockerfile中的常用指令做一个总结。
FROM <image>[:<Tag>] [AS <name>]
- 指明本镜像是基于哪个基础镜像来创建的
- 必须放在Dockerfile第一行
# 以官方 ASP.NET Core镜像(Tag是3.1)作为基础镜像 FROM mcr.microsoft.com/dotnet/core/aspnet:3.1 AS build-env
RUN <command>
RUN ["executable","arg1","arg2"]
- 运行shell命令的两种写法,第一种可当作Windows上的 cmd /S /C 或者Linux上的 /bin/sh -c ,第二种是exec form,要注意的是参数会被传递成Json,所以不要忘记双引号。
- 注意的是每运行一个RUN命令都会在当前镜像的顶层生成一个新层,并将结果提交。
RUN dotnet restore RUN ["dotnet", "restore"]
CMD ["executable","param1","param2"]
CMD ["param1","param2"]
CMD command param1 param2
- 定义镜像创建的容器默认执行的命令,理论上一个Dockerfile中只有一个CMD指令,如果存在多个只执行最后一条。
- CMD指令有3中写法,第二种是最为ENTRYPOINT的参数
- 要注意区别RUN和CMD,CMD指令在创建镜像的过程中不会执行,是在容器创建好后执行的。
- 当Docker执行run命令并附加了CMD时,我们在Dockerfile中指定的CMD将会被重写。
CMD echo "This is a test container."
LABEL <key>=<value> <key>=<value> <key>=<value> ...
- LABEL指令为镜像添加键值对形式的元数据
- 如果在键值对中需要写入空格,要使用双引号或者反斜杠
- 若想查看镜像中定义的label,可使用 docker image inspect --format='' <image> 命令
LABEL version="1.0" LABEL maintainer="xuhy"
EXPOSE <port> [<port>/<protocol>...]
- 用来声明容器运行时监听的网络端口
- 并且可以指定监听的协议时TCP或是UDP,默认时采用TCP
- 注意当使用EXPOSE指定指定端口后其实并不会在容器运行时就自动开启这个端口,主要是针对镜像使用者作为一个声明,在启动容器还是需要使用 -p 标识来映射端口。
EXPOSE 80/tcp EXPOSE 80/udp
docker run -p 80:80/tcp -p 80:80/udp ...
ENV <key> <value>
ENV <key>=<value> ...
- 设置环境变量,参数使用键值对的形式增加
- 后续的所有指令包括RUN都可以使用它所创建的环境变量
- 使用该镜像创建的容器中也可以使用该环境变量,可以使用 docker inspect 命令来查看环境变量,并可以使用 docker run --env <key>=<value> 来修改这些环境变量。
ENV myName John Doe
ENV myDog Rex The Dog
ENV myCat fluffy
ADD [--chown=<user>:<group>] <src>... <dest>
ADD [--chown=<user>:<group>] ["<src>",... "<dest>"]
- 将 <src> 中指定的路径/文件(也可以是远端文件的URL)拷贝到镜像中的文件系统的 <dest> 路径。
- <src> 中指定的路径或者文件可以使用通配符
- --chown 特性只在创建Linux容器时起作用
# 添加所有以 "hom" 开头的文件 ADD hom* /mydir/ # ? 替换任何单个的字符, e.g., "home.txt" ADD hom?.txt /mydir/ # 添加 "test" 到 `WORKDIR`/relativeDir/ ADD test relativeDir/ # 添加 "test" 到 /absoluteDir/ ADD test /absoluteDir/ # 当添加特殊字符如“[”时,需要遵循Go语言的规则对其转移,如拷贝文件“ arr[0].txt”时使用 ADD arr[[]0].txt /mydir/
COPY [--chown=<user>:<group>] <src>... <dest>
COPY [--chown=<user>:<group>] ["<src>",... "<dest>"]
- COPY指令与上面的ADD指令基本功能一样
COPY hom* /mydir/ COPY hom?.txt /mydir/ COPY test.txt relativeDir/ COPY test.txt /absoluteDir/ ADD arr[[]0].txt /mydir/
ENTRYPOINT command param1 param2
ENTRYPOINT ["executable", "param1", "param2"]
- 使用ENTRYPOINT指令可以将容器配置成一个可执行文件,这样就不能通过docker run中的命令行所覆盖,但是可以通过参数传递的方式影响到容器的执行
- 当定义了ENTRYPOINT后,CMD只能够作为参数进行传递
- 每个Dockerfile理论上只有一个ENTRYPOINT,若定义了多个ENTRYPOINT只有最后一个起作用
ENTRYPOINT ["dotnet", " MyWebApi.dll "]
VOLUME ["/data"]
- 为镜像创建一个挂载点供创建容器时用卷来挂载
VOLUME ["/usr/data/mysql"]
VOLUME ["/var/data1","/var/data2"]
USER <user>[:<group>]
USER <UID>[:<GID>]
- 设定运行RUN或者CMD的用户名或用户ID
# Set it for subsequent commands USER xuhy
WORKDIR /path/to/workdir
- 为后续的 RUN , CMD , ENTRYPOINT COPY 指令设定工作目录
- 如果指定的路径不存在会自动创建
- WORKDIR 在Dockerfile中可以多次使用,如果提供的是一个相对路径,那么这个相对路径会被指定为前一个 WORKDIR 指令的相对路径下
WORKDIR /a WORKDIR b WORKDIR c # pwd命令的工作目录在“/a/b/c”下 RUN pwd
ARG <name>[=<default value>]
- 该指令用来定义参数变量,定义了参数后,可以在创建镜像是使用 docker build 命令时使用 --build-arg <varname>=<value> 标识来为参数赋值。
- 在定义参数时也可为其设定默认值 ARG user1=someusr
- 一个 ARG 变量是在Dockerfile中定义的行之后才开始生效
- 如果在同个Dockerfile中使用 ARG 和 ENV 定义了同名变量,那么使用 ENV 定义的环境变量会覆盖 ARG 定义的参数
- ARG 定义的变量的作用域只在当前构建层中有效,也就是说在使用 RUN 命令之后便超出了其作用域
ARG user1="abc" ARG CONT_IMG_VER = "v1.0" ENV CONT_IMG_VER v1.0.0 #同名变量,ENV会覆盖ARG的变量 USER $user1 #... $ docker build --build-arg user=what_user,CONT_IMG_VER=v2.0.1
构建ASP.NET Core项目镜像
接下来我们尝试定制一个ASP.NET Core应用的镜像,当前版本的Visual Studio和Visual Studio Code都已经集成了Docker插件,真不愧时宇宙级IDE。使用VS创建一个测试项目“MyWebApp.MVC”,可以看到在创建项目时便可以同时创建Dockerfile,只需勾选“启动Docker”支持即可。如下图所示。
等待VS生成要默认脚手架代码后,试运行没问题后在项目上右键 → 添加 → Docker支持,随后目标OS选择Linux。随后VS会帮我们生成一份Dockerfile文件,并且会自动执行镜像的构建和容器的创建执行,全自动挡。
既然已经学习了Dockerfile的命令,让我们把VS生成的Dockerfile过目一遍。
1 # 拉取微软官方提供的asp.net core运行时镜像 2 FROM mcr.microsoft.com/dotnet/core/aspnet:3.1-buster-slim AS base 3 WORKDIR /app #指定当前工作目录 4 EXPOSE 5000 #声明暴露5000端口 5 6 # 拉取微软官方提供的.NET Core SDK 镜像,用于编译和发布项目 7 FROM mcr.microsoft.com/dotnet/core/sdk:3.1-buster AS build 8 WORKDIR /src #指定当前工作目录 9 COPY ["MyWebApp.MVC/MyWebApp.MVC.csproj", "MyWebApp.MVC/"] #项目文件拷贝到镜像中的“/src/MyWebApp.MVC/”目录中 10 RUN dotnet restore "MyWebApp.MVC/MyWebApp.MVC.csproj" #执行命令来恢复项目的依赖项 11 COPY . . #把当前目录(windows主机)的所有内容拷贝到镜像的当前目录“/src/” 12 WORKDIR "/src/MyWebApp.MVC" #切换当前工作目录 13 RUN dotnet build "MyWebApp.MVC.csproj" -c Release -o /app/build #使用.net core编译器执行“生成” 14 15 # 基于上面生成项目用的镜像“build”继续搭建本层,工作目录为“/src/MyWebApp.MVC/” 16 FROM build AS publish 17 RUN dotnet publish "MyWebApp.MVC.csproj" -c Release -o /app/publish 18 # 至此项目的编译与发布已完成,就是说现在生成了“MyWebApp.MVC.dll”文件了 19 20 FROM base AS final 21 WORKDIR /app 22 COPY --from=publish /app/publish . #将刚刚发布项目的publish文件夹下的所有文件拷贝到本镜像的当前目录下(/app),注意结尾的"."代表当前目录 23 ENTRYPOINT ["dotnet", "MyWebApp.MVC.dll"] #启动网站
通过powershell来执行镜像的命令,先进入项目文件夹的目录,执行命令。
PS D:workspaceMyWebApp.MVC > docker build -f "${pwd}MyWebApp.MVCDockerfile" --force-rm -t mywebappmvc:test .
其中 -f 后跟dockerfile的路径, --force-rm 表示删除中间层镜像, -t 后跟“镜像名:Tag”,最后跟“.”表示当前工作路径或上下文(context)为当前路径。
等待16个Step(Dockerfile中每一句命令是一个Step)执行完成,可以检查下当前镜像列表中多出刚刚构建的镜像。
Successfully built 9ae226b0a4e9 Successfully tagged mywebappmvc:test PS D:workspaceMyWebApp.MVC> docker image ls
REPOSITORY TAG IMAGE ID CREATED SIZE mywebappmvc test 9ae226b0a4e9 22 seconds ago 212MB
在本地试执行,将容器的5000端口映射到本地的8080端口。
docker run -dt -p 8080:5000 --name aspnetcore.test mywebappmvc:test
执行成功后在浏览器中访问8080端口,不出意外便可以看到asp.net core mvc的默认页面。
可执行 docker logs aspnetcore.test 查看容器执行的日志。
我们可以将创建好的Docker镜像上传到DockerHub上,首先需要我们在DockerHub上建立自己的账户,在本地Docker上登录 docker login <username> 。登录成功之后便可以使用 docker push <image> 命令上传镜像。注意镜像名称的书写有格式要求,要按照 docker push username/image:tag 的。本地可以使用 docker tag 命令来标记本地镜像。
docker tag [OPTIONS] IMAGE[:TAG] [REGISTRYHOST/][USERNAME/]NAME[:TAG]
在Linux服务器上运行容器
1. 在Linux上安装Docker
Linux上安装Docker也很简单,直接参照官方文档一步步执行即可,没啥坑。由于是在一台全新的Ubuntu系统上安装,先更新 apt ,下载和更新必要的包
~$ sudo apt-get update ~$ sudo apt-get install apt-transport-https ca-certificates curl gnupg-agent software-properties-common
搭建本地Repository
~$ curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add - ~$ sudo apt-key fingerprint 0EBFCD88 ~$ sudo add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable"
安装Docker Engine,执行下面命令安装最新版Docker Engine
$ sudo apt-get update $ sudo apt-get install docker-ce docker-ce-cli containerd.io
安装完成之后试一下命令
~$ docker --version Docker version 19.03.8, build afacb8b7f0 ~$ sudo docker run --rm hello-world
2.拉取镜像并运行容器
首先拉取刚刚上传到DockerHub上的镜像
~$ sudo docker pull mark826/mywebappmvc:test20515
查看下本地Docker镜像可以看到拉取成功
~$ sudo docker images REPOSITORY TAG IMAGE ID CREATED SIZE mark826/mywebappmvc test20515 9ae226b0a4e9 4 hours ago 212MB
接着和在Windows下一样运行容器即可。
~$ sudo docker run -dt -p 8080:5000 --name aspnetcore.test mark826/mywebappmvc:test20515
现在访问服务器的8080端口便可以访问asp.net core的页面了。
参考文献
[1] 官文:https://docs.docker.com/engine/reference/builder/
[3] 介绍Dockerfile的博文:https://www.cnblogs.com/jsonhc/p/7766841.html
[4] 博文:https://www.cnblogs.com/edisonchou/p/dockerfile_inside_introduction.html
[5] Ubuntu安装Docker:https://docs.docker.com/engine/install/ubuntu/
[6] 官文介绍Docker容器化ASP.NET Core应用:https://docs.docker.com/engine/examples/dotnetcore/