什么是docker镜像
Docker镜像是由文件系统叠加而成。
- 最底端是一个引导文件系统,即bootfs:
- 这很像典型的Linux/Unix的引导文件系统。Docker用户几乎永远不会和引导文件系统有什么交互。实际上,当一个容器启动后,它将会被移到内存中,而引导文件系统则会被卸载(unmount),以留出更多的内存供initrd磁盘镜像使用。
- Docker镜像的第二层是root文件系统rootfs:
- 它位于引导文件系统之上。rootfs可以是一种或多种操作系统(如Debian或者Ubuntu文件系统)。
在传统的Linux引导过程中,root文件系统会最先以只读的方式加载,当引导结束并完成了完整性检查之后,它才会被切换为读写模式。但是在Docker里,root文件系统永远只能是只读状态,并且Docker利用联合加载(union mount)技术又会在root文件系统层上加载更多的只读文件系统。联合加载指的是一次同时加载多个文件系统,但是在外面看起来只能看到一个文件系统。联合加载会将各层文件系统叠加到一起,这样最终的文件系统会包含所有底层的文件和目录。
Docker将这样的文件系统称为镜像。一个镜像可以放到另一个镜像的顶部。位于下面的镜像称为父镜像(parent image),可以一次类推,直到镜像栈的最底部,最底部的镜像称为基础镜像(base image)。最后,当从一个镜像启动容器时,Docker会在该镜像的最顶层加载一个读写文件系统。我们想在Docker中运行的程序就是在这个读写层中执行。
当Docker第一次启动一个容器时,初始的读写层是空的。当文件系统发生变化时,这些变化都会应用到这一层上。比如,如果想要修改一个文件,这个文件首先会从该读写层下面的只读层复制到读写层。该文件的只读层版本依然存在,但是已经被读写层中的该文件副本所隐藏。通常这种机制被称为写时复制(copy on write),这也是使Docker如此强大的技术之一。每个只读镜像层都是只读的,并且以后永远不会变化。当创建一个新容器时,Docker会构建出一个镜像栈,并在栈的最顶端添加一个读写层。这个读写层再加上其下面的镜像层以及一些配置数据,就构成了一个容器。在上一章我们已经知道,容器可以修改,它们有自己的状态,并且是可以启动和停止的。容器的这种特点加上镜像分层框架(image-layering framework),使我们可以快速构建镜像并运行包含我们自己的应用程序和服务的容器。
镜像管理
列出镜像:
通过docker images命令列出docker主机上可用的镜像:
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest d13c942271d6 2 weeks ago 72.8M
在之前我们执行了docker run命令的同时将ubuntu的镜像也下载到了本地,本地镜像都保存在Docker宿主机的/var/lib/docker目录下。镜像从仓库下载下来,而仓库在Registry中。默认的Registry是由Docker公司运营的公共Registry服务,即Docker Hub。https://hub.docker.com/
我们可以通过docker pull来拉去我们想要的镜像,比如说Centos:
# docker pull centos
Using default tag: latest
latest: Pulling from library/centos
a1d0c7532777: Pull complete
Digest: sha256:a27fd8080b517143cbbbab9dfb7c8571c40d67d534bbdee55bd6c573f432b177
Status: Downloaded newer image for centos:latest
docker.io/library/centos:latest
# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
ubuntu latest d13c942271d6 2 weeks ago 72.8MB
centos latest 5d0da3dc9764 4 months ago 231MB
我们虽然称之为Ubuntu操作系统,但实际上它并不是一个完整的操作系统。它只是一个裁剪版,只包含最低限度的支持系统运行的组件。
为了区分同一个仓库中的不同镜像,Docker提供了一种称为标签(tag)的功能。每个镜像在列出来时都带有一个标签,每个标签对组成特定镜像的一些镜像层标记。这种机制使得在同一个仓库中可以存储多个镜像。我们可以通过在仓库名后面加上一个冒号和标签名来指定该仓库中的某一个镜像。用docker run命令从镜像启动一个容器时,如果该镜像不在本地,Docker会先从Docker Hub下载该镜像。如果没有指定具体的镜像标签,那么Docker会自动下载latest标签的镜像。
查找镜像:
通过docker search命令来查找所有Docker Hub上公共的可用镜像:
# docker search puppet
NAME DESCRIPTION STARS OFFICIAL AUTOMATED
buildkite/puppeteer A Puppeteer Docker image based on Puppeteer’… 85 [OK]
alekzonder/puppeteer GoogleChrome/puppeteer image and screenshots… 82 [OK]
devopsil/puppet Dockerfile for a container with puppet insta… 31 [OK]
macadmins/puppetmaster Simple puppetmaster based on CentOS 6 26 [OK]
zenato/puppeteer-renderer Puppeteer(Chrome headless node API) based we… 17 [OK]
zenato/puppeteer Base environment image for Puppeteer (Headle… 8 [OK]
camptocamp/puppetserver Puppetlabs's puppetserver 7 [OK]
...
这条命令会完成镜像查找工作,返回如下信息:
- 仓库名;
- 镜像描述;
- 用户评论(Stars)——反映出一个镜像的受欢迎程度;
- 是否官方(Official)——由上游开发者管理的镜像;
- 自动构建(Automated)——表示这个镜像是由Docker Hub的自动构建(Automated Build)流程创建的。
## 这条命令将会下载devopsil/puppet镜像到本地
# docker pull devopsil/puppet
## 构建容器
# docker run -i -t devopsil/puppet /bin/bash
[root@1a7d3fb64b61 /]# facter
architecture => x86_64
augeasversion => 1.0.0
## 查看版本
#puppet --version
3.7.4
这里我们以交互的方式启动了该容器,并且在里面运行了Bash shell。在进入容器shell之后,我们运行了Facter(puppet的主机探测应用),它也是预安装在镜像之内的。最后,在容器里,我们运行了puppet程序以验证puppet是否安装正常。
上面已经介绍了如何拉取已经构建好的带有定制内容的Docker镜像,那么如何构建自己的镜像呢?
构建Docker镜像有以下两种方法:
- 使用docker commit命令。
- 使用docker build命令和 Dockerfile 文件。
在这里并不推荐使用docker commit来构建镜像,而应该使用更灵活、更强大的Dockerfile来构建Docker镜像。但是为了对Docker有一个更全面的了解,还是会先介绍以下如何使用docker commit构建Docker镜像。之后将重点介绍Docker所推荐的镜像构建方法:编写Dockerfile之后使用docker build命令。
一般来说,我们不是真正的“创建”新镜像,而是基于一个已有的基础镜像,如ubuntu或centos等,构建新镜像而已。如果真的想从零构建一个全新的镜像,也可以参考https://docs.docker.com/engine/userguide/eng-image/baseimages/。
通过commit命令创建镜像
docker commit 构建镜像可以想象为是在往版本控制系统里提交变更。我们先创建一个容器,并在容器里做出修改,就像修改代码一样,最后再将修改提交为一个镜像。
# docker run -i -t ubuntu /bin/bash
root@09a97b511ef2:/# apt-get update
root@09a97b511ef2:/# apt-get install apache2
我们启动了一个容器,并在里面安装了Apache。我们会将这个容器作为一个Web服务器来运行,所以我们想把它的当前状态保存下来。这样我们就不必每次都创建一个新容器并再次在里面安装Apache了。为了完成此项工作,需要先使用exit命令从容器里退出,之后再运行docker commit命令:
# docker commit 09a97b511ef2 test_tag/apache2
sha256:47d0a2e94688fd88b2c00e6a91014244e1c25191c78eaba322d6103abce3157c
# docker images test_tag/apache2
REPOSITORY TAG IMAGE ID CREATED SIZE
test_tag/apache2 latest 47d0a2e94628 57 seconds ago 219MB
在使用docker commit命令中,指定了要提交的修改过的容器的ID(可以通过docker ps命令得到刚创建的容器ID),以及一个目标镜像仓库和镜像名,这里是test_tag/apahce2。需要注意的是,docker commit提交的只是创建容器的镜像与容器的当前状态之间有差异的部分,这使得该更新非常轻量。通过docker images 可以查看新创建的镜像信息。
也可以在提交镜像时指定更多的数据(包括标签)来详细描述所做的修改。
# docker commit -m="A new custom image" --author="Yun Zhou" e437fge4s921 test_tag/apache2:webserver
这条命令里,我们指定了更多的信息选项:
- -m 用来指定创建镜像的提交信息;
- --author 用来列出该镜像的作者信息;
- 最后在test/apache2后面增加了一个webserver标签。
通过使用docker inspect命令来查看新创建的镜像的详细信息,如果想从刚创建的镜像运行一个容器,可以使用docker run命令:
# docker inspect test_tag/apache2:webserver
# docker run -t -i test_tag/apache2:webserver /bin/bash
创建Dockerfile文件
下面将介绍如何通过Dockerfile的定义文件和docker build命令来构建镜像。
Dockerfile使用基本的基于DSL语法的指令来构建一个Docker镜像,之后使用docker build命令基于该Dockerfile中的指令构建一个新的镜像。
# mkdir /opt/static_web
# cd /opt/static_web/
# vim Dockerfile
首先创建一个名为static_web的目录用来保存Dockerfile,这个目录就是我们的构建环境(build environment),Docker则称此环境为上下文(context)或者构建上下文(build context)。Docker会在构建镜像时将构建上下文和该上下文中的文件和目录上传到Docker守护进程。这样Docker守护进程就能直接访问你想在镜像中存储的任何代码、文件或者其他数据。这里我们还创建了一个Dockerfile文件,我们将用它构建一个能作为Web服务器的Docker镜像。
# Version: 0.0.1
FROM ubuntu:latest
MAINTAINER Yun Zhou "zy129129129@126.com"
RUN apt-get update
RUN apt-get install -y nginx
RUN echo 'Hi, I am in your container' > /usr/share/nginx/html/index.html
EXPOSE 80
Dockerfile由一系列指令和参数组成。每条指令都必须为大写字母,切后面要跟随一个参数。Dockerfile中的指令会按照顺序从上到下执行,所以应该根据需要合理安排指令的顺序。每条指令都会创建一个新的镜像层并对镜像进行提交。Docker大体上按照如下流程执行Dockerfile中的指令。
- Docker从基础镜像运行一个容器。
- 执行第一条指令,对容器进行修改。
- 执行类似docker commit的操作,提交一个新的镜像层。
- Docker再基于刚提交的镜像运行一个新的容器。
- 执行Dockerfile中的下一条命令,直到所有指令都执行完毕。
从上面可以看出,如果你的Dockerfile由于某些原因(如某条指令失败了)没有正常结束,那你也可以得到一个可以使用的镜像。这对调试非常有帮助:可以基于该镜像运行一个具备交互功能的容器,使用最后创建的镜像对为什么你的指令会失败进行调试。
Dockerfile也支持注释。以#开头的行都会被认为是注释,# Version: 0.0.1这就是个注释
FROM:
每个Dockerfile的第一条指令都应该是FROM。FROM指令指定一个已经存在的镜像,后续指令都是将基于该镜像进行,这个镜像被称为基础镜像(base iamge)。在这里ubuntu:latest就是作为新镜像的基础镜像。也就是说Dockerfile构建的新镜像将以ubuntu:latest操作系统为基础。在运行一个容器时,必须要指明是基于哪个基础镜像在进行构建。
MAINTAINER:
MAINTAINER指令,这条指令会告诉Docker该镜像的作者是谁,以及作者的邮箱地址。这有助于表示镜像的所有者和联系方式
RUN:
在这些命令之后,我们指定了三条RUN指令。RUN指令会在当前镜像中运行指定的命令。这里我们通过RUN指令更新了APT仓库,安装nginx包,并创建了一个index.html文件。像前面说的那样,每条RUN指令都会创建一个新的镜像层,如果该指令执行成功,就会将此镜像层提交,之后继续执行Dockerfile中的下一个指令。
默认情况下,RUN指令会在shell里使用命令包装器/bin/sh -c 来执行。如果是在一个不支持shell的平台上运行或者不希望在shell中运行(比如避免shell字符串篡改),也可以使用exec格式的RUN指令,通过一个数组的方式指定要运行的命令和传递给该命令的每个参数:
RUN ["apt-get", "install", "-y", "nginx"]
EXPOSE:
EXPOSE指令是告诉Docker该容器内的应用程序将会使用容器的指定端口。这并不意味着可以自动访问任意容器运行中服务的端口。出于安全的原因,Docker并不会自动打开该端口,而是需要你在使用docker run运行容器时来指定需要打开哪些端口。
可以指定多个EXPOSE指令来向外部公开多个端口,Docker也使用EXPOSE指令来帮助将多个容器链接,在后面的学习过程中我们会接触到。
基于Dockerfile构建新镜像
执行docker build命令时,Dockerfile中的所有指令都会被执行并且提交,并且在该命令成功结束后返回一个新镜像。
命令中最后的“.”告诉Docker到当前目录中去找Dockerfile文件。
# cd static_web
# docker build -t="test/static_web" .
Sending build context to Docker daemon 2.048kB
Step 1/6 : FROM ubuntu:latest
---> d13c942271d6
Step 2/6 : MAINTAINER Yun Zhou "zy129129129@126.com"
---> Running in aa3cddeaab63
Removing intermediate container aa3cddeaab63
---> 192d9031bdb4
Step 3/6 : RUN apt-get update
...
Successfully built 93cd76acfa44
Successfully tagged test/static_web:latest
##再回到docker build过程。可以看到构建上下文已经上传到Docker守护进程:Sending build context to Docker daemon 2.048kB
-t选项为新镜像设置了仓库和名称,这里仓库为test,镜像名为static_web。建议为自己的镜像设置合适的名字方便以后追踪和管理
也可以在构建镜像的过程当中为镜像设置一个标签v1:
# docker build -t="test/static_web:v1" .
也可以指定一个Git仓库地址来指定Dockerfile的位置,这里Docker假设在Git仓库的根目录下存在Dockerfile文件:
# docker build -t="test/static_web:v1" git@github.com:test/static_web
提示:如果在构建上下文的根目录下存在以.dockerignore命名的文件的话,那么该文件内容会被按行进行分割,每一行都是一条文件过滤匹配模式。这非常像.gitignore文件,该文件用来设置哪些文件不会被上传到构建上下文中去。该文件中模式的匹配规则采用了Go语言中的filepath。
之后,可以看到Dockerfile中的每条指令会被顺序执行,而作为构建过程中最终结果,返回了新镜像的ID,即93cd76acfa44。构建的每一步及其对应指令都会独立运行,并且在输出最终镜像ID之前,Docker会提交每步的构建结果。
指令失败时会怎样?
假设我们将安装的软件包名字弄错,比如写成ngin,再次运行docker build:
# docker build -t="test/static_web" .
Sending build context to Docker daemon 2.048kB
Step 1/6 : FROM ubuntu:latest
---> d13c942271d6
Step 2/6 : MAINTAINER Yun Zhou "zy129129129@126.com"
---> Running in aa3cddeaab63
Removing intermediate container aa3cddeaab63
---> 192d9031bdb4
Step 3/6 : RUN apt-get update
Reading package lists...
Building dependency tree...
Reading state information...
E: Unable to locate package ngin
The command '/bin/sh -c apt-get install -y ngin' returned a non-zero code: 100
这时我们需要调试一下这次失败,我们可以通过docker run命令来基于这次构建到目前为止已经成功的最后一步创建一个容器,这里它的ID是e38dg5138w5a:
# docker run -t -i e38dg5138w5a /bin/bash
# apt-get install -y ngin
Reading package lists... Done
Building dependency tree
Reading state information... Done
E: Unable to locate package ngin
再次运行出错的指令apt-get install -y ngin,发现这里没有找到ngin包,我们执行安装nginx包时,包命输错了。这时退出容器使用正确的包名修改Dockerfile文件,之后再尝试进行构建。
构建缓存:
在上面执行构建镜像的过程中,我们发现当执行apt-get update时,返回Using cache。Docker会将之前的镜像层看做缓存,因为在安装nginx前并没有做其他的修改,因此Docker会将之前构建时创建的镜像当做缓存并作为新的开始点。然后,有些时候需要确保构建过程不会使用缓存。可以使用docker build 的 --no-cache标志。
# docker build --no-cache -t="test/static_web" .
构建缓存带来的一个好处就是,我们可以实现简单的Dockerfile模板(比如在Dockerfile文件顶部增加包仓库或者更新包,从而尽可能确保缓存命中)。
- NV 在镜像中设置环境变量,在这里设置了一个名为REFRESHED_AT的环境变量,这个环境变量用来表明该镜像模板最后的更新时间,这样只需要修改ENV指令中的日期,这使Docker在命中ENV指令时开始重置这个缓存,并运行后续指令而无需依赖该缓存。也就是说,RUN apt-get update这条指令将会被再次执行,包缓存也将会被刷新为最新内容。
查看新镜像:
现在来看一下新构建的镜像,使用docker images命令,如果想深入探求镜像如何构建出来的,可以使用docker history命令看到新构建的test/static_web镜像的每一层,以及创建这些层的Dockerfile指令。
#/opt/static_web# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
test/static_web latest 93cd76acfa44 11 minutes ago 165MB
test_tag/apache2 latest 47d0a2e94688 56 minutes ago 219MB
ubuntu latest d13c942271d6 2 weeks ago 72.8MB
centos latest 5d0da3dc9764 4 months ago 231MB
devopsil/puppet latest 31a474a2334e 3 years ago 321MB
root@VM-16-9-ubuntu:/opt/static_web# docker images test/static_web
REPOSITORY TAG IMAGE ID CREATED SIZE
test/static_web latest 93cd76acfa44 12 minutes ago 165MB
#/opt/static_web# docker history 93cd76acfa44
IMAGE CREATED CREATED BY SIZE COMMENT
93cd76acfa44 14 minutes ago /bin/sh -c #(nop) EXPOSE 80 0B
be5a9a2e6d7c 14 minutes ago /bin/sh -c echo 'Hi, I am in your container'… 27B
733d0521aa04 14 minutes ago /bin/sh -c apt-get install -y nginx 59.2MB
5a3f35d1220b 14 minutes ago /bin/sh -c apt-get update 33.2MB
192d9031bdb4 14 minutes ago /bin/sh -c #(nop) MAINTAINER Yun Zhou "zy12… 0B
d13c942271d6 2 weeks ago /bin/sh -c #(nop) CMD ["bash"] 0B
<missing> 2 weeks ago /bin/sh -c #(nop) ADD file:122ad323412c2e70b… 72.8MB
从新镜像启动容器
下面基于新构建的镜像启动一个新容器,来检查之前的构建工作是否一切正常:
root@VM-16-9-ubuntu:/opt/static_web# docker run -d -p 80 --name static_web test/static_web nginx -g "daemon off;"
e77a8ea44c65f225c9de78ec9e825d7d3a655f271ae9fb2se0a1761f0108e877
root@VM-16-9-ubuntu:/opt/static_web# docker ps -l
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e77a8ea44c65 test/static_web "nginx -g 'daemon of…" 3 seconds ago Up 2 seconds 0.0.0.0:49153->80/tcp, :::49153->80/tcp static_web
- -d选项,告诉Docker以分离(detached)的方式在后台运行。这种方式非常适合运行类似Nginx守护进程这样的需要长时间运行的进程。
- 这里也指定了需要在容器中运行的命令:nginx -g "daemon off;"。这将以前台运行的方式启动Nginx,来作为我们的Web服务器。
- -p选项,控制Docker在运行时应该公开哪些网络端口给外部(宿主机)。运行一个容器时,Docker可通过两种方法在宿主机上分配端口。
- Docker可以在宿主机上通过/proc/sys/net/ipv4/ip_local_port_range文件随机一个端口映射到容器的80端口。
- 可以在Docker宿主机中指定一个具体的端口号来映射到容器的80端口上。
-
这将在Docker宿主机上随机打开一个端口,这个端口会连接到容器中的80端口上。docker ps命令可查看容得的端口分配情况
如果没有启动成功,则通过交互的方式进入我们新创建的镜像中,尝试启动nginx,通过分析错误日志查出不能正常启动的原因,在这里我遇到的问题是:
nginx: [emerg] socket() [::]:80 failed (97: Address family not supported by protocol)
我们需要删除/etc/nginx/sites-enabled/default 中 listen [::]:80 ipv6only=on default_server;定位到问题,我们退出容器,重新修改我们的Dockerfile:
# Version: 0.0.1
FROM ubuntu:latest
MAINTAINER Name "mail address"
ENV REFRESHED_AT 2022-01-21
RUN apt-get update
RUN apt-get install -y nginx
RUN echo 'Hi, I am in your container' > /usr/share/nginx/html/index.html
RUN sed -i '22d' /etc/nginx/sites-enabled/default
EXPOSE 80
重新尝试构建我们的容器,再次启动我们新建的容器,通过docker ps -l查看是否正常启动了。
我们也可以通过docker port 来查看容器的端口映射情况:
root@VM-16-9-ubuntu:/opt/static_web# docker port e77a8ea44c65
80/tcp -> 0.0.0.0:49153
80/tcp -> :::49153
在上面的命令中我们指定了想要查看映射情况的容器ID和容器的端口号,这里是80。该命令返回了宿主机中映射的端口,即49153。
##-p选项还让我们可以灵活地管理容器和宿主机之间的端口映射关系。比如,可以指定将容器中的端口映射到Docker宿主机的某一个特定的端口上:
# docker run -d -p 80:80 --name static_web test/static_web nginx -g "daemon off;"
##上面的命令会将容器内的80端口绑定到本地宿主机的80端口上。我们也可以将端口绑定限制在特定的网络接口(即ip地址)上:
# docker run -d -p 127.0.0.1:80:80 --name static_web test/static_web nginx -g "daemon off;"
##我们也可以使用类似的方法将容器内的80端口绑定到一个特定网络接口的随机端口上:
# docker run -d -p 127.0.0.1::80 --name static_web test/static_web nginx -g "daemon off;"
##Docker还提供了一个更简单的方式,即-P参数,该参数可以用来对外公开在Dockfile中的EXPOSE指令中设置的所有端口:
# docker run -d -P --name static_web test/static_web nginx -g "daemon off;"
2ert32e975ad5e47a487e5e238uwi24da0826886dc24b2497a561d274ik8sj7w
# docker ps -l
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
e77a8ea44c65 test/static_web "nginx -g 'daemon of 4 seconds ago Up 3 seconds 0.0.0.0:49153->80/tcp static_web
该命令会将容器内的80端口对本地宿主机公开,并且绑定到宿主机的一个随机端口上。该命令会将用来构建该镜像的Dockerfile文件中EXPOSE指令指定的其他端口也一并公开。
# curl localhost:32773
Hi, I am in your container
到这,就完成了一个非常简单的基于Docker的Web服务器。
删除镜像
我们可以通过docker rmi命令来删除一个镜像
root@VM-16-9-ubuntu:/opt/static_web# docker rmi test/static_web
Untagged: test/static_web:latest
Deleted: sha256:93cd76acfa44c6a9a897d116d9d1376f91f62c1cd126ab054cfa43799e313b5f
Deleted: sha256:be5a9a2e6d7c3291f35d0793231ec5ef61f5f22220749ae8d8a68b77a9792b54
这里我们删除了test/static_web镜像。在这里也可以看到Docker的分层文件系统:每个Deleted都代表一个镜像层被删除。该操作只会将本地的镜像删除。如果我们想删除本地的所有镜像可以像这样: # docker rmi `docker images -a -q`
运行自己的Docker Registry
前面我们已经介绍了Docker有公共的Docker Registry就是Docker Hub。但是有时我们可能希望构建和存储包含不想被公开的信息或数据的镜像。这时候我们有以下两种选择:
- 利用Docker Hub上的私有仓库;
- 在防火墙后面运行自己的Registry。
从Docker容器安装一个Registry非常简单
## 拉去registry镜像
# docker pull registry
## 搭建本地镜像源
# docker run -d -v /opt/registry:/var/lib/registry -p 5000:5000 --restart=always --name registry registry:latest
## 查看容器状态
# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
f570fab5d67d registry:latest "/entrypoint.sh /etc 3 seconds ago Up 3 seconds 0.0.0.0:5000->5000/tcp registry
接下来将我们的镜像上传到本地的Docker Registry
## 找到我们要上传的镜像
# docker images test_tag/apache2
REPOSITORY TAG IMAGE ID CREATED SIZE
test_tag/apache2 latest 31a474a2334e About an hour ago 219MB
## 使用新的Registry给该镜像打上标签
# docker tag 31a474a2334e test_tag/pup
## 通过docker push 命令将它推送到新的Registry中去
# docker push test_tag/pup
Using default tag: latest
The push refers to repository [docker.io/test_tag/pup]
296931581307: Preparing
672ceee85f2a: Preparing
53395b3fe7e1: Preparing
e0dec291ae94: Preparing
denied: requested access to the resource is denied
## 测试我们上传的镜像
# docker run -it test_tag/pup /bin/bash