## Docker核心概念 ### 1、Docker系统 Docker系统有两个程序:Docker服务端和Docker客户端 > Docker服务端: > > ​ 是一个服务进程,管理着所有的容器。 > > ​ docker engine > > Docker客户端: > > ​ 扮演着docker服务端的远程控制器,可以用来控制docker的服务端进程。 ### 2、Docker核心组件 > Docker 镜像 - Docker images > > Docker 仓库 - Docker registeries > > Docker 容器 - Docker containers **容器组成要素** 名称空间 namespace 资源限制 cgroups 文件系统 overlay2(UnionFS) #### 2.1、Docker 仓库 用来保存镜像,可以理解为代码控制中的代码仓库。同样的,Docker 仓库也有公有和私有的概念。 公有的 Docker 仓库名字是 Docker Hub。Docker Hub提供了庞大的镜像集合供使用。这些镜像可以是自己创建,或者在别人的镜像基础上创建。Docker 仓库是 Docker 的分发部分。 #### 2.2、Docker 镜像 ![image-20200311140939156](D:\云计算强哥\资源\Docker\01-开启新篇章-Docker入门\assets\image-20200311140939156.png) Docker 镜像是 Docker 容器运行时的只读模板,每一个镜像由一系列的层 (layers) 组成。Docker 使用 UnionFS 来将这些层联合到单独的镜像中。UnionFS 允许独立文件系统中的文件和文件夹(称之为分支)被透明覆盖,形成一个单独连贯的文件系统。正因为有了这些层的存在,Docker 是如此的轻量。当你改变了一个 Docker 镜像,比如升级到某个程序到新的版本,一个新的层会被创建。因此,不用替换整个原先的镜像或者重新建立(在使用虚拟机的时候你可能会这么做),只是一个新的层被添加或升级了。现在你不用重新发布整个镜像,只需要升级,层使得分发 Docker 镜像变得简单和快速。 在 Docker 的术语里,一个只读层被称为镜像,一个镜像是永久不会变的。 由于 Docker 使用一个统一文件系统,Docker 进程认为整个文件系统是以读写方式挂载的。 但是所有的变更都发生顶层的可写层,而下层的原始的只读镜像文件并未变化。由于镜像不可写,所以镜像是无状态的。 每一个镜像都可能依赖于由一个或多个下层的组成的另一个镜像。下层那个镜像是上层镜像的父镜像。 **镜像名称组成** ```bash registry/repo:tag ``` **基础镜像** 一个没有任何父镜像的镜像,谓之基础镜像 **镜像ID** 所有镜像都是通过一个 64 位十六进制字符串 (内部是一个 256 bit 的值)来标识的。 为简化使用,前 12 个字符可以组成一个短ID,可以在命令行中使用。短ID还是有一定的碰撞机率,所以服务器总是返回长ID。 #### 2.3、Docker 容器 Docker 容器和文件夹很类似,一个Docker容器包含了所有的某个应用运行所需要的环境。每一个 Docker 容器都是从 Docker 镜像创建的。Docker 容器可以运行、开始、停止、移动和删除。每一个 Docker 容器都是独立和安全的应用平台,Docker 容器是 Docker 的运行部分。 #### 2.4、Docker镜像和容器区别 **Docker镜像** 要理解Docker镜像和docker容器之间的区别,确实不容易。 假设Linux内核是第0层,那么无论怎么运行Docker,它都是运行于内核层之上的。这个Docker镜像,是一个只读的镜像,位于第1层,它不能被修改或不能保存状态。 一个Docker镜像可以构建于另一个Docker镜像之上,这种层叠关系可以是多层的。第1层的镜像层我们称之为基础镜像(Base Image),其他层的镜像(除了最顶层)我们称之为父层镜像(Parent Image)。这些镜像继承了他们的父层镜像的所有属性和设置,并在Dockerfile中添加了自己的配置。 Docker镜像通过镜像ID进行识别。镜像ID是一个64字符的十六进制的字符串。但是当我们运行镜像时,通常我们不会使用镜像ID来引用镜像,而是使用镜像名来引用。 要列出本地所有有效的镜像,可以使用命令 ```shell [root@wxin.com ~]# docker images ``` 镜像可以发布为不同的版本,这种机制我们称之为标签(Tag)。 如上图所示,neo4j镜像有两个版本:lastest版本和2.1.5版本。 可以使用pull命令加上指定的标签: ```shell [root@wxin.com ~]# docker pull ubuntu:14.04 [root@wxin.com ~]# docker pull ubuntu:12.04 ``` **Docker容器** Docker容器可以使用命令创建 ```shell [root@wxin.com ~]# docker run imagename ``` 它会在所有的镜像层之上增加一个可写层。这个可写层有运行在CPU上的进程,而且有两个不同的状态:运行态(Running)和退出态 (Exited)。这就是Docker容器。当我们使用docker run启动容器,Docker容器就进入运行态,当我们停止Docker容器时,它就进入退出态。 当我们有一个正在运行的Docker容器时,从运行态到停止态,我们对它所做的一切变更都会永久地写到容器的文件系统中。要切记,对容器的变更是写入到容器的文件系统的,而不是写入到Docker镜像中的。 我们可以用同一个镜像启动多个Docker容器,这些容器启动后都是活动的,彼此还是相互隔离的。我们对其中一个容器所做的变更只会局限于那个容器本身。 如果对容器的底层镜像进行修改,那么当前正在运行的容器是不受影响的,不会发生自动更新现象。 如果想更新容器到其镜像的新版本,那么必须当心,确保我们是以正确的方式构建了数据结构,否则我们可能会导致损失容器中所有数据的后果。 64字符的十六进制的字符串来定义容器ID,它是容器的唯一标识符。容器之间的交互是依靠容器ID识别的,由于容器ID的字符太长,我们通常只需键入容器ID的前4个字符即可。当然,我们还可以使用容器名,但显然用4字符的容器ID更为简便。 ### 3、名字空间 名字空间是 Linux 内核一个强大的特性。每个容器都有自己单独的名字空间,运行在其中的应用都像是在独立的操作系统中运行一样。名字空间保证了容器之间彼此互不影响。 1. pid 名字空间 不同用户的进程就是通过 pid 名字空间隔离开的,且不同名字空间中可以有相同 pid。所有的 LXC 进程在 Docker中的父进程为Docker进程,每个 LXC 进程具有不同的名字空间。同时由于允许嵌套,因此可以很方便的实现嵌套的 Docker 容器。 2. net 名字空间 有 了 pid 名字空间, 每个名字空间中的 pid 能够相互隔离,但是网络端口还是共享 host 的端口。网络隔离是通过 net 名字空间实现的,每个 net 名字空间有独立的 网络设备, IP 地址, 路由表, /proc/net 目录。这样每个容器的网络就能隔离开来。Docker 默认采用 veth 的方式,将容器中的虚拟网卡同 host 上的一 个Docker 网桥 docker0 连接在一起。 3. ipc 名字空间 容器中进程交互还是采用了 Linux 常见的进程间交互方法(interprocess communication - IPC), 包括信号量、消息队列和共享内存、socket、管道等。然而同 VM 不同的是,容器的进程间交互实际上还是 host 上具有相同 pid 名字空间中的进程间交互,因此需要在 IPC 资源申请时加入名字空间信息,每个 IPC 资源有一个唯一的 32 位 id。 4. mnt名字空间 类似 change root,将一个进程放到一个特定的目录执行。mnt 名字空间允许不同名字空间的进程看到的文件结构不同,这样每个名字空间 中的进程所看到的文件目录就被隔离开了。同 chroot 不同,每个名字空间中的容器在 /proc/mounts 的信息只包含所在名字空间的 mount point。 5. uts 名字空间 UTS("UNIX Time-sharing System") 名字空间允许每个容器拥有独立的 hostname 和 domain name, 使其在网络上可以被视作一个独立的节点而非主机上的一个进程。 6. user 名字空间 每个容器可以有不同的用户和组 id, 也就是说可以在容器内用容器内部的用户执行程序而非主机上的用户。 ## 镜像管理 搜索镜像 这种方法只能用于官方镜像库,例如搜索基于 Centos 操作系统的镜像 ```shell [root@wxin.com ~]# docker search centos ``` 按星级搜索镜像,例如查找 star 数至少为 100 的镜像,默认不加 s 选项找出所有相关 ubuntu 镜像 ``` [root@wxin.com ~]# docker search ubuntu -f stars=100 ``` 拉取镜像 ```shell [root@wxin.com ~]# docker pull centos ``` 查看本地镜像 ```bash [root@wxin.com ~]# docker image list ``` 查看镜像详情 ```bash [root@wxin.com ~]# docker image inspect 镜像id ``` 只查看所有镜像的id ```bash [root@wxin.com ~]# docker images -q ``` 查看镜像制作的过程,相当于Dockerfile ```bash [root@wxin.com ~]# docker history daocloud.io/ubuntu ``` 删除镜像 删除一个或多个,多个之间用空格隔开,可以使用镜像名称或id ```bash [root@wxin.com ~]# docker rmi daocloud.io/library/mysql ``` 强制删除 如果镜像正在被使用,可用--force强制删除 ```bash [root@wxin.com ~]# docker rmi docker.io/ubuntu:latest --force ``` 删除所有镜像 ```bash [root@wxin.com ~]# docker rmi $(docker images -q) ``` 给镜像打tag ```bash [root@wxin.com ~]# docker tag daocloud.io/ubuntu daocloud.io/ubuntu:v1 ``` ## 容器管理 **创建新容器但不启动** ```bash [root@wxin.com ~]# docker create -it daocloud.io/library/centos:5 /bin/bash ``` **创建并运行一个新容器** 同一个镜像可以启动多个容器,每次执行run子命令都会运行一个全新的容器 ``` [root@wxin.com ~]# docker run -itd --name="test" --restart=always centos /bin/bash ``` -i 捕获标准输入输出 -t 分配一个终端或控制台 -d 后台运行 --restart=always 容器随docker engine自启动,因为重启docker时默认容器都会被关闭,也适用于create子命令 --rm 当你仅仅需要短暂的运行一个容器且数据不需保存,你可能就希望Docker能在容器结束时自动清理其所产生的数据。这时就需要--rm参数了。注意:--rm 和 -d不能共用 --name="容器名称" 为容器分配一个名字,如果没有指定,docker会自动分配一个随机名称 可以通过三种方式调用容器: 1)使用UUID长命名("f78375b1c487e03c9438c729345e54db9d20cfa2ac1fc3494b6eb60872e74778") 2)使用UUID短Id("f78375b1c487") 3)使用Name("evil_ptolemy") UUID标识是由Docker deamon生成的。 如果执行docker run时没指定--name,那么deamon会自动生成一个随机字符串。 但是对于一个容器来说有个name会非常方便,当需要连接其它容器时或者需要区分其它容器时,使用容器名可以简化操作。无论容器运行在前台或者后台,这个名字都是有效的。 **保存容器PID** 如果在使用Docker时有自动化的需求,你可以将containerID输出到指定的文件中(PIDfile),类似于某些应用程序将自身ID输出到文件中,方便后续脚本操作。 --cidfile="文件名" **断开与容器的连接且关闭容器** ```bash root@d33c4e8c51f8 /#exit 快捷键ctrl+d 如果只想断开和容器的连接而不关闭容器可使用快捷键ctrl+p+q ``` **ps 查看容器** ```bash [root@wxin.com ~]#docker ps //只查看运行状态的容器 ``` **-a 查看所有容器** ```bash [root@wxin.com ~]#docker ps -a ``` **-q 只查看容器id** ```bash [root@wxin.com ~]# docker ps -a -q ``` **-l 列出最近一次启动的容器(了解)** ``` [root@wxin.com ~]# docker ps -l ``` **inspect 查看容器详细信息** 用于查看容器的配置信息,包含容器名、环境变量、运行命令、主机配置、网络配置和数据卷配置等。 ```bash [root@wxin.com ~]# docker inspect d95 //d95是我机器上运行的一个容器ID的前3个字符 ``` 比如:容器里在安装ip或ifconfig命令之前,查看网卡IP显示容器IP地址和端口号,如果输出是空的说明没有配置IP地址(不同的Docker容器可以通过此IP地址互相访问) ```shell [root@wxin.com ~]# docker inspect --format='{{.NetworkSettings.IPAddress}}' 容器id ``` 关于inspect的用法有很多,如果想多了解可向大强哥索取文档**"扩展-Docker 查看容器信息-格式化.md"** **start 启动容器** ```shell [root@wxin.com ~]# docker start name ``` **stop kill 关闭容器** ```bash [root@wxin.com ~]# docker stop name //正常关闭 [root@wxin.com ~]# docker kill name //强制关闭容器 关闭所有running状态的容器 [root@wxin.com ~]# docker kill $(docker ps -q) ``` **rm 删除容器** ```shell [root@wxin.com ~]# docker rm 容器id或名称 要删除一个运行中的容器,添加 -f 参数 根据状态删除所有容器 [root@wxin.com ~]# docker rm $(docker ps -qf status=exited) ``` **restart 重启容器** ```bash [root@wxin.com ~]# docker restart name ``` **pause 暂停容器** ```bash [root@wxin.com ~]# docker pause name ``` **unpause 恢复容器,与pause相对应** ```bash [root@wxin.com ~]# docker unpause name ``` **rename 修改容器名称** ```bash [root@wxin.com ~]# docker rename mytest test ``` **stats 动态显示容器的资源消耗情况,包括:CPU、内存、网络I/O** ```bash [root@wxin.com ~]# docker rename name ``` **port 输出容器端口与宿主机端口的映射情况** ```bash [root@wxin.com ~]# docker port blog 80/tcp -> 0.0.0.0:80 宿主机的80端口映射到容器blog的内部端口80,这样可通过宿主机的80端口查看容器blog提供的服务 ``` **连接容器** 方法1、attach ```bash [root@wxin.com ~]# docker attach 容器id 前提是容器创建时必须指定了交互shell ``` 方法2、exec 通过exec命令可以创建两种任务:后台型任务和交互型任务 交互型任务 ```bash [root@wxin.com ~]# docker exec -it 容器id /bin/bash root@68656158eb8e:/ ls ``` 后台型任务 ```bash [root@wxin.com ~]# docker exec 容器id touch /testfile ``` **监控容器运行** 可以使用logs、top、events、wait这些子命令 logs 可以通过使用docker logs命令来查看容器的运行日志,其中--tail选项可以指定查看最后几条日志,而-t选项则可以对日志条目附加时间戳。使用-f选项可以跟踪日志的输出,直到手动停止。 ```bash [root@wxin.com ~]# docker logs App_Container //不同终端操作 [root@wxin.com ~]# docker logs -f App_Container [root@wxin.com ~]# docker run -itd --name mytest docker.io/centos /bin/sh -c "while true; do echo hello world; sleep 2; done" 37738fe3d6f9ef26152cb25018df9528a89e7a07355493020e72f147a291cd17 [root@wxin.com ~]# docker logs mytest hello world hello world docker run -itd --name mytest 298ec /bin/sh -c "while true; do echo hello world; sleep 2; done" ``` top 显示一个运行的容器里面的进程信息 ```bash [root@wxin.com ~]# docker top birdben/ubuntu:v1 ``` events Get real time events from the server 实时输出Docker服务器端的事件,包括容器的创建,启动,关闭等。 ```bash [root@wxin.com ~]# docker start loving_meninsky loving_meninsky [root@wxin.com ~]# docker events //不同终端操作 2017-07-08T16:39:23.177664994+08:00 network connect df15746d60ffaad2d15db0854a696d6e49fdfcedc7cfd8504a8aac51a43de6d4 (container=50a0449d7729f94046baf0fe5a1ce2119742261bb3ce8c3c98f35c80458e3e7a, name=bridge, type=bridge) 2017-07-08T16:39:23.356162529+08:00 container start 50a0449d7729f94046baf0fe5a1ce2119742261bb3ce8c3c98f35c80458e3e7a (image=ubuntu, name=loving_meninsky) ``` wait(X) Block until a container stops, then print its exit code --捕捉容器停止时的退出码 执行此命令后,该命令会"hang"在当前终端,直到容器停止,此时,会打印出容器的退出码 ```bash [root@wxin.com ~]# docker wait 01d8aa //不同终端操作 137 ``` diff 查看容器内发生改变的文件,以elated_lovelace容器为例 ```bash [root@68656158eb8e:/]# touch c.txt ``` ```bash 用diff查看: 包括文件的创建、删除和文件内容的改变都能看到 [root@wxin.com ~]# docker diff 容器名称 A /c.txt C对应的文件内容的改变,A对应的均是文件或者目录的创建删除 [root@wxin.com ~]# docker diff 7287 A /a.txt C /etc C /etc/passwd A /run A /run/secrets ``` 宿主机和容器之间相互Copy文件 cp的用法如下: docker cp [OPTIONS] CONTAINER:PATH LOCALPATH docker cp [OPTIONS] LOCALPATH CONTAINER:PATH 如:容器mysql中/usr/local/bin/存在docker-entrypoint.sh文件,可如下方式copy到宿主机 ```bash [root@wxin.com ~]# docker cp mysql:/usr/local/bin/docker-entrypoint.sh /root ``` 修改完毕后,将该文件重新copy回容器 ```bash [root@wxin.com ~]# docker cp /root/docker-entrypoint.sh mysql:/usr/local/bin/ ``` ## 容器镜像制作 ### 1、容器文件系统打包 将容器的文件系统打包成tar文件,也就是把正在运行的容器直接导出为tar包的镜像文件 export Export a container's filesystem as a tar archive 有两种方式(elated_lovelace为容器名) ```shell 第一种: [root@wxin.com ~]# docker export -o elated_lovelace.tar elated_lovelace 第二种: [root@wxin.com ~]# docker export 容器名称 > 镜像.tar ``` 导入镜像归档文件到其他宿主机 import Import the contents from a tarball to create a filesystem image ```shell [root@wxin.com ~]# docker import elated_lovelace.tar elated_lovelace:v1 ``` 注意: > 如果导入镜像时没有起名字,随后可以单独起名字(没有名字和tag),可以手动加tag ```shell [root@wxin.com ~]# docker tag 镜像ID mycentos:7 ``` ### 2、通过容器创建本地镜像 背景: ​ 容器运行起来后,又在里面做了一些操作,并且要把操作结果保存到镜像里 方案: ​ 使用 docker commit 指令,把一个正在运行的容器,直接提交为一个镜像。commit 是提交的意思,类似告诉svn服务器我要生成一个新的版本。 例:在容器内部新建了一个文件 ```shell [root@wxin.com ~]# docker exec -it 4ddf4638572d /bin/sh [root@4ddf4638572d \]# touch test.txt [root@4ddf4638572d \]# exit 将这个新建的文件提交到镜像中保存 [root@wxin.com ~]# docker commit 4ddf4638572d wing/helloworld:v2 ``` 例: ```shell [root@wxin.com ~]# docker commit -m "my images version1" -a "wing" 108a85b1ed99 daocloud.io/ubuntu:v2 sha256:ffa8a185ee526a9b0d8772740231448a25855031f25c61c1b63077220469b057 -m 添加注释 -a 作者author 108a85b1ed99 容器环境id daocloud.io/ubuntu:v2 镜像名称:hub的名称/镜像名称:tag -p,–pause=true 提交时暂停容器运行 ``` Init 层的存在,是为了避免执行 docker commit 时,把 Docker 自己对 /etc/hosts 等文件做的修改,也一起提交掉。 ### 3、镜像迁移 保存一台宿主机上的镜像为tar文件,然后可以导入到其他的宿主机上: save Save an image(s) to a tar archive 将镜像打包,与下面的load命令相对应 ```shell [root@wxin.com ~]# docker save -o nginx.tar nginx [root@wxin.com ~]# docker save > nginx.tar nginx ``` load Load an image from a tar archive or STDIN 与上面的save命令相对应,将上面sava命令打包的镜像通过load命令导入 ```shell [root@wxin.com ~]# docker load < nginx.tar ``` 注: 1.tar文件的名称和保存的镜像名称没有关系 2.导入的镜像如果没有名称,自己打tag起名字 ### 4、通过Dockerfile创建镜像 虽然可以自己制作 rootfs(见'容器文件系统那些事儿'),但Docker 提供了一种更便捷的方式,叫作 Dockerfile docker build命令用于根据给定的Dockerfile和上下文以构建Docker镜像。 **docker build语法** ```shell [root@wxin.com ~]# docker build [OPTIONS] ``` **选项说明** > --build-arg,设置构建时的变量 > > --no-cache,默认false。设置该选项,将不使用Build Cache构建镜像 > > --pull,默认false。设置该选项,总是尝试pull镜像的最新版本 > > --compress,默认false。设置该选项,将使用gzip压缩构建的上下文 > > --disable-content-trust,默认true。设置该选项,将对镜像进行验证 > > --file, -f,Dockerfile的完整路径,默认值为‘PATH/Dockerfile’ > > --isolation,默认--isolation="default",即Linux命名空间;其他还有process或hyperv > > --label,为生成的镜像设置metadata > > --squash,默认false。设置该选项,将新构建出的多个层压缩为一个新层,但是将无法在多个镜像之间共享新层;设置该选项,实际上是创建了新image,同时保留原有image。 > > --tag, -t,镜像的名字及tag,通常name:tag或者name格式;可以在一次构建中为一个镜像设置多个tag > > --network,默认default。设置该选项,Set the networking mode for the RUN instructions during build > > --quiet, -q ,默认false。设置该选项,Suppress the build output and print image ID on success > > --force-rm,默认false。设置该选项,总是删除掉中间环节的容器 > > --rm,默认--rm=true,即整个构建过程成功后删除中间环节的容器 **PATH | URL | -说明** 给出命令执行的上下文。 docker build需要的各种文件必须放到上下文目录中 上下文可以是构建执行所在的本地路径,也可以是远程URL,如Git库、tar包或文本文件等。 如果是Git库,如https://github.com/docker/rootfs.git#container:docker,则隐含先执行git clone --depth 1 --recursive,到本地临时目录;然后再将该临时目录发送给构建进程。 构建镜像的进程中,可以通过ADD命令将上下文中的任何文件加入到镜像中。 -表示通过STDIN给出Dockerfile或上下文。 示例: ```shell docker build - < Dockerfile ``` 说明:该构建过程只有Dockerfile,没有上下文 ```shell docker build - < context.tar.gz ``` 说明:其中Dockerfile位于context.tar.gz的根路径 可以同时设置多个tag ```shell [root@wxin.com ~]# docker build -t champagne/bbauto:latest -t champagne/bbauto:v2.1 . ``` 创建镜像所在的文件夹和Dockerfile文件 ```shell mkdir dockerfile-test cd dockerfile-test touch Dockerfile ``` 在Dockerfile文件中写入指令,每一条指令都会更新镜像的信息例如: ```dockerfile [root@wxin.com ~]# vim Dockerfile # This is a comment FROM daocloud.io/library/centos:7 MAINTAINER wing wing@wxin.com RUN 命令1;命令2;命令3 RUN 命令 ``` **格式说明** > ​ 每行命令都是以 INSTRUCTION statement 形式,就是命令+ 清单的模式。命令要大写,"#"是注解。 > > ​ FROM 命令是告诉docker 我们的镜像什么。 > > ​ MAINTAINER 是描述 镜像的创建人。 > > ​ RUN 命令是在镜像内部执行。就是说他后面的命令应该是针对镜像可以运行的命令。 **创建镜像** ```shell [root@wxin.com ~]# docker build -t centos:v2 . build 是docker创建镜像的命令 -t 镜像名称 "." 上下文目录,如果不用-f指定Dockerfile文件的位置和名称,默认会从上下文目录也就是这里指定的当前目录查找 ``` 创建完成后,用这个镜像运行容器 ```shell [root@wxin.com ~]# docker run -it centos:v2 /bin/bash ``` **--build-arg应用示例** ```shell # cat Dockerfile FROM daocloud.io/library/nginx:1.7.9 MAINTAINER wing wing@qq.com ARG TEST ARG TEST1 RUN echo $TEST > /a.txt RUN echo $TEST1 > /b.txt # docker build --build-arg TEST=123 --build-arg TEST1=456 -t nginx:v11 . ``` ### 5、例01-容器化Python的Flask应用 **目标** ​ 用 Docker 部署一个用 Python 编写的 Web 应用。 ​ 私有镜像仓库 OP ​ 下载python RD ​ 运行成容器 RD ​ 写代码 RD ​ 打包tar RD ​ 上传tar包到私有仓库 RD ​ 拉取带应用的镜像到测试服 OP ​ 发版上线 OP **应用代码部分** ​ 代码功能:如果当前环境中有"NAME"这个环境变量,就把它打印在"Hello"后,否则就打印"Hello world",最后再打印出当前环境的 hostname。 ```python [root@wxin.com ~]# mkdir python_app [root@wxin.com ~]# cd python_app [root@wxin.com ~]# vim app.py from flask import Flask import socket import os app = Flask(__name__) @app.route('/') def hello(): html = "

Hello {name}!

Hostname: {hostname}
" return html.format(name=os.getenv("NAME", "world"), hostname=socket.gethostname()) if __name__ == "__main__": app.run(host='0.0.0.0', port=80) ``` **代码测试** ```bash # yum search pip # yum install python2-pip 或者 # yum install python3-pip # pip3 install flask # python3 app.py ``` 应用依赖 ​ 定义在同目录下的 requirements.txt 文件里,内容如下: ```shell [root@wxin.com ~]# vim requirements.txt Flask ``` 编写Dockerfile ```dockerfile [root@wxin.com ~]# vim Dockerfile FROM daocloud.io/library/python:3 WORKDIR /app ADD . /app #RUN pip install --trusted-host pypi.python.org -r requirements.txt RUN pip install -i http://mirrors.aliyun.com/pypi/simple/ --trusted-host mirrors.aliyun.com -r requirements.txt EXPOSE 80 ENV NAME World # ENTRYPOINT ['/bin/sh','-c'] #新版本20.10.17的ENTRYPOINT有问题,可能会报找不到命令错误 CMD ["python", "app.py"] ``` ![image-20200722153133293](C:\Users\A\Pictures\docker\1.png) **Dockerfile文件说明** FROM python:2.7-slim ​ 使用官方提供的 Python 开发镜像作为基础镜像 ​ 指定"python:2.7-slim"这个官方维护的基础镜像,从而免去安装 Python 等语言环境的操作。否则,这一段就得这么写了: ​ FROM ubuntu:latest ​ RUN apt-get update -y ​ RUN apt-get install -y python-pip python-dev build-essential WORKDIR /app ​ 将工作目录切换为 /app,意思是在这一句之后,Dockerfile 后面的操作都以这一句指定的 /app 目录作为当前目录。 ADD . /app 将当前目录下的所有内容复制到 /app 下,Dockerfile 里的原语并不都是指对容器内部的操作。比如 ADD,指的是把当前目录(即 Dockerfile 所在的目录)里的文件,复制到指定容器内的目录当中。 RUN pip install --trusted-host pypi.python.org -r requirements.txt 使用 pip 命令安装这个应用所需要的依赖 EXPOSE 80 //允许外界访问容器的 80 端口 ENV NAME World //设置环境变量 CMD ["python", "app.py"] 设置容器进程为:python app.py,即:这个 Python 应用的启动命令 这里app.py 的实际路径是 /app/app.py。CMD ["python", "app.py"] 等价于 "docker run python app.py"。 在使用 Dockerfile 时,可能还会看到一个叫作 ENTRYPOINT 的原语。它和 CMD 都是 Docker 容器进程启动所必需的参数,完整执行格式是:"ENTRYPOINT CMD"。 但是,默认,Docker 会提供一个隐含的 ENTRYPOINT,即:/bin/sh -c。所以,在不指定 ENTRYPOINT 时,比如在这个例子里,实际上运行在容器里的完整进程是:/bin/sh -c "python app.py",即 CMD 的内容就是 ENTRYPOINT 的参数。 基于以上原因,后面会统一称 Docker 容器的启动进程为 ENTRYPOINT,而不是 CMD。 现在目录结构: ```shell [root@wxin.com ~]# ls Dockerfile app.py requirements.txt ``` **构建镜像** ```shell [root@wxin.com ~]# docker build -t helloworld . -t 给这个镜像加一个 Tag ``` Dockerfile 中的每个原语执行后,都会生成一个对应的镜像层。即使原语本身并没有明显地修改文件的操作(比如,ENV 原语),它对应的层也会存在。只不过在外界看来,这个层是空的。 查看结果 ```shell [root@wxin.com ~]# docker image ls REPOSITORY TAG IMAGE ID helloworld latest 653287cdf998 ``` 启动容器 ```shell [root@wxin.com ~]# docker run -p 4000:80 helloworld 镜像名 helloworld 后面,什么都不用写,因为在 Dockerfile 中已经指定了 CMD。 否则,就得把进程的启动命令加在后面: [root@wxin.com ~]# docker run -p 4000:80 helloworld python app.py ``` 查看容器 ```shell [root@wxin.com ~]# docker ps CONTAINER ID IMAGE COMMAND CREATED 4ddf4638572d helloworld "python app.py" 10 seconds ago ``` 进入容器 ```shell [root@wxin.com ~]# docker exec -it b69 /bin/bash ``` 访问容器内应用 ```shell [root@wxin.com ~]# curl http://localhost:4000 ```

Hello World!

Hostname: 4ddf4638572d
至此,已经使用容器完成了一个应用的开发与测试,如果现在想要把这个容器的镜像上传到 DockerHub 上分享给更多的人,首先要注册一个 Docker Hub 账号,然后使用 docker login 命令登录。 给容器镜像打tag起一个完整的名字: ```shell [root@wxin.com ~]# docker tag helloworld yanqiang20072008/helloworld:v1 yanqiang20072008为我在docker hub的用户名 ``` 推送镜像到Docker Hub ```shell [root@wxin.com ~]# docker push yanqiang20072008/helloworld:v1 ``` ### 6、例02-部署docker官方文档 ```bash # docker run -d -p 8888:80 docker/getting-started ``` ### 7、例03-部署node.js网站 **注意:构建镜像的过程非常缓慢,建议有时间的时候再尝试此实验** 访问上例中部署的网站下载app.zip压缩包 ![image-20220719001900850](C:\Users\A\Pictures\docker\2.png) 文件已经下载到wing docker文档目录 在解压后的app目录中创建Dockerfile文件内容如下 ```dockerfile # syntax=docker/dockerfile:1 FROM node:12-alpine RUN apk add --no-cache python g++ make WORKDIR /app COPY . . RUN yarn install --production CMD ["node", "src/index.js"] ``` 构建镜像 ```bash 注意:构建镜像的过程非常缓慢,建议有时间的时候再尝试此实验 # docker build -t getting-started . ``` 启动容器 ```bash # docker run -dp 3000:3000 getting-started ``` 访问应用 ``` 浏览器http://localhost:3000 ``` ![image-20210512150612818](C:\Users\A\Pictures\docker\3.png) Update the source code In the `src/static/js/app.js` file, update line 56 to use the new empty text. ```js 去掉此行

No items yet! Add one above!

添加本行

You have no todo items yet! Add one above!

``` Let’s build our updated version of the image, using the same command we used before. ``` # docker build -t getting-started . ``` Let’s start a new container using the updated code. ``` # docker run -dp 3000:3000 getting-started ``` 查看结果 ![image-20210512152020632](C:\Users\A\Pictures\docker\4.png) ### 8、例04-制作Kubectl的镜像 注:本实例学完k8s之后才能做 制作镜像:wing/kubectl ```dockerfile [root@wxin.com ~]#cat Dockerfile/kubectl/Dockerfile FROM alpine MAINTAINER wing <276267003@qq.com> LABEL org.label-schema.vcs-ref=$VCS_REF \ org.label-schema.vcs-url="https://github.com/vfarcic/kubectl" \ org.label-schema.docker.dockerfile="/Dockerfile" ENV KUBE_LATEST_VERSION="v1.13.0" RUN apk add --update ca-certificates && \ apk add --update -t deps curl && \ curl -L https://storage.googleapis.com/kubernetes-release/release/${KUBE_LATEST_VERSION}/bin/linux/amd64/kubectl -o /usr/local/bin/kubectl && \ chmod +x /usr/local/bin/kubectl && \ apk del --purge deps && \ rm /var/cache/apk/* CMD ["kubectl", "help"] ``` ### 9、Dockerfile优化 #### 控制镜像大小 编译一个简单的nginx成功以后发现好几百M。 1) 选择合适的基础镜像: 选择一个轻量级、经过优化的基础镜像作为起点。官方提供的基础镜像通常非常小且已经经过优化,如 Alpine、Scratch 等。 2) 最小化安装软件包: 只安装应用程序所需的最小化软件包,避免不必要的依赖。删除不需要的软件包、文件和文档,以减小镜像大小。 3) 使用多阶段构建: 使用多阶段构建可以减小最终镜像的大小。在构建过程中,可以使用一个镜像作为构建环境,然后从中提取构建好的结果并将其复制到一个较小的镜像中。 4) 使用 .dockerignore 文件: 使用 .dockerignore 文件来排除不需要复制到镜像中的文件和目录,以减少构建上下文的大小,从而减小镜像大小。 5) 减少镜像层数: 减少镜像的层数可以减小镜像的大小。将多个操作合并为一个步骤,避免不必要的层。 RUN 命令要尽量写在一条里,每次 RUN 命令都是在之前的镜像上封装,只会增大不会减小 6) 清理临时文件和缓存: 构建过程中产生的临时文件和缓存可能会增加镜像大小,在构建完成后进行清理。每次进行依赖安装后使用yum clean all清除缓存中的rpm头文件和包文件 #### 使用构建缓存 Docker使用构建缓存可以显著加快Docker镜像的构建速度。当你在Dockerfile中执行指令时,Docker会检查它之前的指令是否已经执行过,并且是否存在缓存,如果存在缓存,Docker将重用缓存而不是重新执行指令。 以下是几个在Dockerfile中使用构建缓存的常用技巧: 1. 按顺序列出指令。将常用但不容易变更的指令,如`FROM`、`LABEL`、`ENV`等,放在Dockerfile的顶部,将变更频繁的指令,如`ADD`、`COPY`、`RUN`等,放在Dockerfile的底部。这样可以最大程度地利用缓存,减少不必要的重新构建。 2. 限制缓存范围。可以使用`COPY`指令的`--chown`参数或`RUN`指令的`--mount`参数来限制缓存的范围。例如,`COPY --chown=root:root file.txt /tmp`将限制缓存仅适用于该指令所复制的文件,而不适用于其他文件或目录。这样可以避免由于其他文件或目录的更改而不必要地重新构建。 3. 避免更新不必要的依赖项。在使用`RUN`指令安装依赖项时,可以指定版本号,以避免不必要的更新。例如,`RUN apt-get update && apt-get install -y python3=3.8.2-1ubuntu1`将只安装指定版本的Python3,而不是最新版本,从而节省构建时间。 4. 在需要清除缓存时使用`--no-cache`选项。有时,缓存可能会导致错误或意外的结果。在这种情况下,可以使用`docker build --no-cache`命令来禁用缓存,并从头开始重新构建Docker镜像。 以上是一些常见的使用构建缓存的技巧,可以帮助你优化Docker镜像的构建速度,提高效率。