36

Docker Dockerfile 指令详解与实战案例

 3 years ago
source link: http://www.cnblogs.com/zhanglianghhh/p/13172747.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

Dockerfile介绍及常用指令,包括FROM,RUN,还提及了 COPY,ADD,EXPOSE,WORKDIR等,其实 Dockerfile 功能很强大,它提供了十多个指令。

Dockerfile介绍

Dockerfile 是一个用来构建镜像的文本文件,文本内容包含了一条条构建镜像所需的指令和说明。

在Docker中创建镜像最常用的方式,就是使用Dockerfile。Dockerfile是一个Docker镜像的描述文件,我们可以理解成火箭发射的A、B、C、D…的步骤。 Dockerfile其内部包含了一条条的指令,每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

FROM 指令-指定基础镜像

所谓定制镜像,那一定是以一个镜像为基础,在其上进行定制。而 FROM 就是指定基础镜像,因此一个 Dockerfile 中 FROM 是必备的指令,并且必须是第一条指令。如下:

FROM centos

MAINTAINER 维护者信息

该镜像是由谁维护的

MAINTAINER lightzhang [email protected]

ENV 设置环境变量

格式有两种:

ENV <key> <value>
ENV <key1>=<value1> <key2>=<value2>...

这个指令很简单,就是设置环境变量而已,无论是后面的其它指令,如 RUN,还是运行时的应用,都可以直接使用这里定义的环境变量。

ENV VERSION=1.0 DEBUG=on \
    NAME="Happy Feet"

这个例子中演示了如何换行,以及对含有空格的值用双引号括起来的办法,这和 Shell 下的行为是一致的。

下列指令可以支持环境变量展开:

ADD、COPY、ENV、EXPOSE、FROM、LABEL、USER、WORKDIR、VOLUME、STOPSIGNAL、ONBUILD、RUN。

可以从这个指令列表里感觉到,环境变量可以使用的地方很多,很强大。通过环境变量,我们可以让一份 Dockerfile 制作更多的镜像,只需使用不同的环境变量即可。

ARG 构建参数

格式:ARG <参数名>[=<默认值>]

构建参数和 ENV 的效果一样,都是设置环境变量。所不同的是,ARG 所 设置的构建环境的环境变量 ,在将来容器运行时是不会存在这些环境变量的。但是不要因此就使用 ARG 保存密码之类的信息,因为 docker history 还是可以看到所有值的。

Dockerfile 中的 ARG 指令是定义参数名称,以及定义其默认值。该 默认值可以在构建命令 docker build 中用 --build-arg <参数名>=<值> 来覆盖

在 1.13 之前的版本,要求 --build-arg 中的参数名,必须在 Dockerfile 中用 ARG 定义过了,换句话说,就是 --build-arg 指定的参数,必须在 Dockerfile 中使用了。如果对应参数没有被使用,则会报错退出构建。

从 1.13 开始,这种严格的限制被放开,不再报错退出,而是显示警告信息,并继续构建。这对于使用 CI 系统,用同样的构建流程构建不同的 Dockerfile 的时候比较有帮助,避免构建命令必须根据每个 Dockerfile 的内容修改。

RUN 执行命令

RUN 指令是用来执行命令行命令的。由于命令行的强大能力,RUN 指令在定制镜像时是最常用的指令之一。其格式有两种:

shell 格式:RUN <命令>,就像直接在命令行中输入的命令一样。如下:

RUN echo '<h1>Hello, Docker!</h1>' > /usr/share/nginx/html/index.html

exec 格式:RUN [“可执行文件”, “参数1”, “参数2”],这更像是函数调用中的格式。

Dockerfile 中每一个指令都会建立一层,RUN 也不例外。每一个 RUN 的行为,都会新建立一层,在其上执行这些命令,执行结束后,commit 这一层的修改,构成新的镜像。

Dockerfile 不推荐写法:

 1 FROM debian:stretch
 2 
 3 RUN apt-get update
 4 RUN apt-get install -y gcc libc6-dev make wget
 5 RUN wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz"
 6 RUN mkdir -p /usr/src/redis
 7 RUN tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1
 8 RUN make -C /usr/src/redis
 9 RUN make -C /usr/src/redis install
10 RUN rm redis.tar.gz

上面的这种写法,创建了 8 层镜像。这是完全没有意义的,而且很多运行时不需要的东西,都被装进了镜像里,比如编译环境、更新的软件包等等。最后一行即使删除了软件包,那也只是当前层的删除;虽然我们看不见这个包了,但软件包却早已存在于镜像中并一直跟随着镜像,没有真正的删除。

结果就是产生非常臃肿、非常多层的镜像,不仅仅增加了构建部署的时间,也很容易出错。 这是很多初学 Docker 的人常犯的一个错误。

另外: Union FS 是有最大层数限制的,比如 AUFS,曾经是最大不得超过 42 层,现在是不得超过 127 层。

Dockerfile 正确写法:

 1 FROM debian:stretch
 2 
 3 RUN buildDeps='gcc libc6-dev make wget' \
 4     && apt-get update \
 5     && apt-get install -y $buildDeps \
 6     && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \
 7     && mkdir -p /usr/src/redis \
 8     && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \
 9     && make -C /usr/src/redis \
10     && make -C /usr/src/redis install \
11     && rm -rf /var/lib/apt/lists/* \
12     && rm redis.tar.gz \
13     && rm -r /usr/src/redis \
14     && apt-get purge -y --auto-remove $buildDeps

这里没有使用很多个 RUN 对应不同的命令,而是仅仅使用一个 RUN 指令,并使用 && 将各个所需命令串联起来。将之前的 8 层,简化为了 1 层,且后面删除了不需要的包和目录。在撰写 Dockerfile 的时候,要经常提醒自己,这并不是在写 Shell 脚本,而是在定义每一层该如何构建。 因此镜像构建时,一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。

很多人初学 Docker 制作出了很臃肿的镜像的原因之一,就是忘记了每一层构建的最后一定要清理掉无关文件。

COPY 复制文件

格式:

1 COPY [--chown=<user>:<group>] <源路径>... <目标路径>
2 COPY [--chown=<user>:<group>] ["<源路径1>",... "<目标路径>"]

COPY 指令将从构建上下文目录中 <源路径> 的文件/目录复制到新的一层的镜像内的 <目标路径> 位置。如:

COPY package.json /usr/src/app/

<源路径> 可以是多个,甚至可以是通配符,其通配符规则要满足 Go 的 filepath.Match 规则,如:

1 COPY hom* /mydir/
2 COPY hom?.txt /mydir/

<目标路径> 可以是 容器内 的绝对路径,也可以是相对于工作目录的相对路径(工作目录可以用 WORKDIR 指令来指定)。 目标路径不需要事先创建,如果目录不存在会在复制文件前先行创建缺失目录。

此外,还需要注意一点,使用 COPY 指令,源文件的各种元数据都会保留。比如读、写、执行权限、文件变更时间等。这个特性对于镜像定制很有用。特别是构建相关文件都在使用 Git 进行管理的时候。

在使用该指令的时候还可以加上 --chown=<user>:<group> 选项来改变文件的所属用户及所属组。

1 COPY --chown=55:mygroup files* /mydir/
2 COPY --chown=bin files* /mydir/
3 COPY --chown=1 files* /mydir/
4 COPY --chown=10:11 files* /mydir/

ADD 更高级的复制文件

ADD 指令和 COPY 的格式和性质基本一致。但是在 COPY 基础上增加了一些功能。

如果 <源路径> 为一个 tar 压缩文件的话,压缩格式为 gzip, bzip2 以及 xz 的情况下,ADD 指令将会 自动解压缩 这个压缩文件到 <目标路径> 去。

在某些情况下,如果我们真的是 希望复制个压缩文件进去,而不解压缩 ,这时就不可以使用 ADD 命令了。

在 Docker 官方的 Dockerfile 最佳实践文档 中要求,尽可能的使用 COPY,因为 COPY 的语义很明确,就是复制文件而已,而 ADD 则包含了更复杂的功能,其行为也不一定很清晰。最适合使用 ADD 的场合,就是所提及的需要自动解压缩的场合。

特别说明:在 COPY 和 ADD 指令中选择的时候,可以遵循这样的原则,所有的文件复制均使用 COPY 指令,仅在需要自动解压缩的场合使用 ADD。

在使用该指令的时候还可以加上 --chown=<user>:<group> 选项来改变文件的所属用户及所属组。

1 ADD --chown=55:mygroup files* /mydir/
2 ADD --chown=bin files* /mydir/
3 ADD --chown=1 files* /mydir/
4 ADD --chown=10:11 files* /mydir/

WORKDIR 指定工作目录

格式为 WORKDIR <工作目录路径>

使用 WORKDIR 指令可以来指定工作目录(或者称为当前目录), 以后各层的当前目录就被改为指定的目录 ,如该 目录不存在,WORKDIR 会帮你建立目录。

之前提到一些初学者常犯的错误是把 Dockerfile 等同于 Shell 脚本来书写,这种错误的理解还可能会导致出现下面这样的错误:

1 RUN cd /app
2 RUN echo "hello" > world.txt

如果将这个 Dockerfile 进行构建镜像运行后,会发现找不到 /app/world.txt 文件,或者其内容不是 hello。原因其实很简单,在 Shell 中,连续两行是同一个进程执行环境,因此前一个命令修改的内存状态,会直接影响后一个命令;而 在 Dockerfile 中,这两行 RUN 命令的执行环境根本不同,是两个完全不同的容器。 这就是对 Dockerfile 构建分层存储的概念不了解所导致的错误。

之前说过每一个 RUN 都是启动一个容器、执行命令、然后提交存储层文件变更。第一层 RUN cd /app 的执行仅仅是当前进程的工作目录变更,一个内存上的变化而已,其结果不会造成任何文件变更。而到第二层的时候,启动的是一个全新的容器,跟第一层的容器更完全没关系,自然不可能继承前一层构建过程中的内存变化。

因此如果需要改变以后各层的工作目录的位置,那么应该使用 WORKDIR 指令。

USER 指定当前用户

格式:USER <用户名>[:<用户组>]

USER 指令和 WORKDIR 相似,都是 改变环境状态并影响以后的层 。WORKDIR 是改变工作目录,USER 则是改变之后层的执行 RUN, CMD 以及 ENTRYPOINT 这类命令的身份。

当然,和 WORKDIR 一样,USER 只是帮助你切换到指定用户而已,这个用户必须是事先建立好的,否则无法切换。

1 RUN groupadd -r redis && useradd -r -g redis redis
2 USER redis
3 RUN [ "redis-server" ]

如果以 root 执行的脚本,在执行期间希望改变身份,比如希望以某个已经建立好的用户来运行某个服务进程,不要使用 su 或者 sudo,这些都需要比较麻烦的配置,而且在 TTY 缺失的环境下经常出错。建议使用 gosu。

1 # 建立 redis 用户,并使用 gosu 换另一个用户执行命令
2 RUN groupadd -r redis && useradd -r -g redis redis
3 # 下载 gosu
4 RUN wget -O /usr/local/bin/gosu "https://github.com/tianon/gosu/releases/download/1.7/gosu-amd64" \
5     && chmod +x /usr/local/bin/gosu \
6     && gosu nobody true
7 # 设置 CMD,并以另外的用户执行
8 CMD [ "exec", "gosu", "redis", "redis-server" ]

VOLUME 定义匿名卷

格式为:

1 VOLUME ["<路径1>", "<路径2>"...]
2 VOLUME <路径>

之前我们说过,容器运行时应该尽量保持容器存储层不发生写操作,对于数据库类需要保存动态数据的应用,其数据库文件应该保存于卷(volume)中。为了防止运行时用户忘记将动态文件所保存目录挂载为卷,在 Dockerfile 中,我们可以事先指定某些目录挂载为匿名卷,这样在运行时如果用户不指定挂载,其应用也可以正常运行,不会向容器存储层写入大量数据。

VOLUME /data

这里的 /data 目录就会在运行时自动挂载为匿名卷,任何向 /data 中写入的信息都不会记录进容器存储层,从而保证了容器存储层的无状态化。当然,运行时可以覆盖这个挂载设置。比如:

docker run -d -v mydata:/data xxxx

在这行命令中,就使用了 mydata 这个命名卷挂载到了 /data 这个位置,替代了 Dockerfile 中定义的匿名卷的挂载配置。

EXPOSE 声明端口

格式为 EXPOSE <端口1> [<端口2>...]

EXPOSE 指令是声明运行时容器提供服务端口,这只是一个声明,在运行时并不会因为这个声明应用就会开启这个端口的服务。在 Dockerfile 中写入这样的声明有两个好处,一个是帮助镜像使用者理解这个镜像服务的守护端口,以方便配置映射;另一个用处则是在运行时使用随机端口映射时,也就是 docker run -P 时,会自动随机映射 EXPOSE 的端口。

要将 EXPOSE 和在运行时使用 -p <宿主端口>:<容器端口> 区分开来。-p,是映射宿主端口和容器端口,换句话说,就是将容器的对应端口服务公开给外界访问,而 EXPOSE 仅仅是声明容器打算使用什么端口而已,并不会自动在宿主进行端口映射。

ENTRYPOINT 入口点

ENTRYPOINT 的格式和 RUN 指令格式一样,分为 exec 格式 和  shell 格式

ENTRYPOINT 的目的和 CMD 一样,都是在指定容器启动程序及参数。ENTRYPOINT 在运行时也可以替代,不过比 CMD 要略显繁琐,需要通过 docker run 的参数 --entrypoint 来指定。

当指定了 ENTRYPOINT 后,CMD 的含义就发生了改变,不再是直接的运行其命令,而是将 CMD 的内容作为参数【★★★★★】传给 ENTRYPOINT 指令, 换句话说实际执行时,将变为:

<ENTRYPOINT> "<CMD>"

那么有了 CMD 后,为什么还要有 ENTRYPOINT 呢?这种 <ENTRYPOINT> “<CMD>“ 有什么好处么?让我们来看几个场景。

场景一:让镜像变成像命令一样使用

假设我们需要一个得知自己当前公网 IP 的镜像,那么可以先用 CMD 来实现:

1 FROM centos:7.7.1908
2 CMD [ "curl", "-s", "ifconfig.io" ]

假如我们使用 docker build -t myip . 来构建镜像的话,如果我们需要查询当前公网 IP,只需要执行:

1 $ docker run myip
2 183.226.75.148

嗯,这么看起来好像可以直接把镜像当做命令使用了,不过命令总有参数,如果我们希望加参数呢?比如从上面的 CMD 中可以看到实质的命令是 curl,那么如果我们希望显示 HTTP 头信息,就需要加上 -i 参数。那么我们可以直接加 -i 参数给 docker run myip 么?

1 $ docker run myip -i
2 docker: Error response from daemon: OCI runtime create failed: container_linux.go:348: starting container process caused "exec: \"-i\": executable file not found in $PATH": unknown.
3 ERRO[0000] error waiting for container: context canceled

我们可以看到可执行文件找不到的报错,executable file not found。之前我们说过, 跟在镜像名后面的是 command 【Usage: docker run [OPTIONS] IMAGE [COMMAND] [ARG…]】, 运行时会替换 CMD 的默认值 。因此这里的 -i 替换了原来的 CMD,而不是添加在原来的 curl -s ifconfig.io 后面。而 -i 根本不是命令,所以自然找不到。

那么如果我们希望加入 -i 这参数,我们就必须重新完整的输入这个命令:

$ docker run myip curl -s ifconfig.io -i

这显然不是很好的解决方案,而使用 ENTRYPOINT 就可以解决这个问题。现在我们重新用 ENTRYPOINT 来实现这个镜像:

1 FROM centos:7.7.1908
2 ENTRYPOINT [ "curl", "-s", "ifconfig.io" ]

使用 docker build -t myip2 . 构建完成后,这次我们再来尝试直接使用 docker run myip2 -i:

 1 $ docker run myip2 
 2 183.226.75.148
 3 
 4 $ docker run myip2 -i
 5 HTTP/1.1 200 OK
 6 Date: Sun, 19 Apr 2020 02:20:48 GMT
 7 Content-Type: text/plain; charset=utf-8
 8 Content-Length: 15
 9 Connection: keep-alive
10 Set-Cookie: __cfduid=d76a2e007bbe7ec2d230b0a6636d115151587262848; expires=Tue, 19-May-20 02:20:48 GMT; path=/; domain=.ifconfig.io; HttpOnly; SameSite=Lax
11 CF-Cache-Status: DYNAMIC
12 Server: cloudflare
13 CF-RAY: 586326015c3199a1-LAX
14 alt-svc: h3-27=":443"; ma=86400, h3-25=":443"; ma=86400, h3-24=":443"; ma=86400, h3-23=":443"; ma=86400
15 cf-request-id: 0231d614d9000099a1e10d7200000001
16 
17 183.226.75.148

可以看到,这次成功了。这是因为当存在 ENTRYPOINT 后,CMD 的内容将会作为参数传给 ENTRYPOINT,而这里 -i 就是新的 CMD,因此会作为参数传给 curl,从而达到了我们预期的效果。

场景二:应用运行前的准备工作

启动容器就是启动主进程,但有些时候,启动主进程前,需要一些准备工作。

比如 mysql 类的数据库,可能需要一些数据库配置、初始化的工作,这些工作要在最终的 mysql 服务器运行之前解决。

此外,可能希望避免使用 root 用户去启动服务,从而提高安全性,而在 启动服务前还需要以 root 身份执行一些必要的准备工作,最后切换到服务用户身份启动服务。 或者除了服务外,其它命令依旧可以使用 root 身份执行,方便调试等。

这些准备工作是和容器 CMD 无关的,无论 CMD 是什么,都需要事先进行一个预处理的工作。这种情况下,可以写一个脚本,然后放入 ENTRYPOINT 中去执行,而这个脚本会将接到的参数(也就是 <CMD>)作为命令,在脚本最后执行。比如官方镜像 redis 中就是这么做的:

1 FROM alpine:3.4
2 ...
3 RUN addgroup -S redis && adduser -S -G redis redis
4 ...
5 ENTRYPOINT ["docker-entrypoint.sh"]
6 
7 EXPOSE 6379
8 CMD [ "redis-server" ]

可以看到其中为了 redis 服务创建了 redis 用户,并在最后指定了 ENTRYPOINT 为 docker-entrypoint.sh 脚本。

1 #!/bin/sh
2 ...
3 # allow the container to be started with `--user`
4 if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
5     chown -R redis .
6     exec su-exec redis "$0" "$@"
7 fi
8 
9 exec "$@"

该脚本的内容就是根据 CMD 的内容来判断,如果是 redis-server 的话,则切换到 redis 用户身份启动服务器,否则依旧使用 root 身份执行。比如:

1 $ docker run -it redis id
2 uid=0(root) gid=0(root) groups=0(root)

CMD 容器启动命令

CMD 指令的格式和 RUN 相似,也是两种格式和一种特殊格式:

1 shell 格式:CMD <命令>
2 exec 格式:CMD ["可执行文件", "参数1", "参数2"...]
3 参数列表格式:CMD ["参数1", "参数2"...]。在指定了 ENTRYPOINT 指令后,用 CMD 指定具体的参数。

之前介绍容器的时候曾经说过,Docker 不是虚拟机,容器就是进程。既然是进程,那么在启动容器的时候,需要指定所运行的程序及参数。CMD 指令就是用于指定默认的容器主进程的启动命令的。

在指令格式上,一般推荐使用 exec 格式,这类格式在解析时会被解析为 JSON 数组,因此一定要使用双引号 “,而不要使用单引号。

如果使用 shell 格式的话,实际的命令会被包装为 sh -c 的参数的形式进行执行。比如:

CMD echo $HOME

在实际执行中,会将其变更为:

CMD [ "sh", "-c", "echo $HOME" ]

这就是为什么我们可以使用环境变量的原因,因为这些环境变量会被 shell 进行解析处理。

提到 CMD 就不得不提 容器中应用在前台执行和后台执行的问题 。这是初学者常出现的一个混淆。

Docker 不是虚拟机,容器中的应用都应该以前台执行,而不是像虚拟机、物理机里面那样,用 systemd 去启动后台服务, 容器内没有后台服务的概念

对于容器而言,其启动程序就是容器应用进程,容器就是为了主进程而存在的 ,主进程退出,容器就失去了存在的意义,从而退出,其它辅助进程不是它需要关心的东西。

一些初学者将 CMD 写为:

CMD service nginx start

使用 service nginx start 命令,则是希望 upstart 来以后台守护进程形式启动 nginx 服务。而刚才说了 CMD service nginx start 会被理解为 CMD [ “sh”, “-c”, “service nginx start”],因此主进程实际上是 sh。那么当 service nginx start 命令结束后,sh 也就结束了,sh 作为主进程退出了,自然就会令容器退出。

正确的做法是直接执行 nginx 可执行文件,并且要求以前台形式运行。比如:

CMD ["nginx", "-g", "daemon off;"]

构建镜像案例-Nginx

构建文件

 1 [root@docker01 make03]# pwd
 2 /root/docker_test/make03
 3 [root@docker01 make03]# ll
 4 total 12
 5 -rw-r--r-- 1 root root 720 Apr 19 16:46 Dockerfile
 6 -rw-r--r-- 1 root root  95 Apr 19 16:19 entrypoint.sh
 7 -rw-r--r-- 1 root root  22 Apr 19 16:18 index.html
 8 [root@docker01 make03]# cat Dockerfile   # Dockerfile文件
 9 # 基础镜像
10 FROM centos:7.7.1908
11 
12 # 维护者
13 MAINTAINER lightzhang [email protected]
14 
15 # 命令:做了些什么操作
16 RUN echo 'nameserver 223.5.5.5' > /etc/resolv.conf && echo 'nameserver 223.6.6.6' >> /etc/resolv.conf
17 RUN curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo && curl -o /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo
18 RUN yum install -y nginx-1.16.1 && yum clean all
19 RUN echo "daemon off;" >> /etc/nginx/nginx.conf
20 
21 # 添加文件
22 COPY index.html /usr/share/nginx/html/index.html
23 COPY entrypoint.sh /usr/local/bin/entrypoint.sh
24 
25 # 对外暴露端口声明
26 EXPOSE 80
27 
28 # 执行
29 ENTRYPOINT ["sh", "entrypoint.sh"]
30 
31 # 执行命令
32 CMD ["nginx"]
33 
34 [root@docker01 make03]# cat index.html   # 访问文件
35 nginx in docker, html
36 [root@docker01 make03]# cat entrypoint.sh   # entrypoint 文件
37 #!/bin/bash
38 if [ "$1" = 'nginx' ]; then
39     exec nginx -c /etc/nginx/nginx.conf
40 fi
41 exec "$@"

构建镜像

 1 [root@docker01 make03]# docker build -t base/nginx:1.16.1 .
 2 Sending build context to Docker daemon  4.608kB
 3 Step 1/11 : FROM centos:7.7.1908
 4  ---> 08d05d1d5859
 5 Step 2/11 : MAINTAINER lightzhang [email protected]
 6  ---> Using cache
 7  ---> 1dc29e78d94f
 8 Step 3/11 : RUN echo 'nameserver 223.5.5.5' > /etc/resolv.conf && echo 'nameserver 223.6.6.6' >> /etc/resolv.conf
 9  ---> Using cache
10  ---> 19398ad9b023
11 Step 4/11 : RUN curl -o /etc/yum.repos.d/CentOS-Base.repo http://mirrors.aliyun.com/repo/Centos-7.repo && curl -o /etc/yum.repos.d/epel.repo http://mirrors.aliyun.com/repo/epel-7.repo
12  ---> Using cache
13  ---> b2451c5856c5
14 Step 5/11 : RUN yum install -y nginx-1.16.1 && yum clean all
15  ---> Using cache
16  ---> 291f27cae4df
17 Step 6/11 : RUN echo "daemon off;" >> /etc/nginx/nginx.conf
18  ---> Using cache
19  ---> 115e07b6313e
20 Step 7/11 : COPY index.html /usr/share/nginx/html/index.html
21  ---> Using cache
22  ---> 9d714d2e2a84
23 Step 8/11 : COPY entrypoint.sh /usr/local/bin/entrypoint.sh
24  ---> Using cache
25  ---> b16983911b56
26 Step 9/11 : EXPOSE 80
27  ---> Using cache
28  ---> d8675d6c2d43
29 Step 10/11 : ENTRYPOINT ["sh", "entrypoint.sh"]
30  ---> Using cache
31  ---> 802a1a67db37
32 Step 11/11 : CMD ["nginx"]
33  ---> Using cache
34  ---> f2517b4d5510
35 Successfully built f2517b4d5510
36 Successfully tagged base/nginx:1.16.1

发布容器与端口查看

 1 [root@docker01 ~]# docker run -d -p 80:80 --name mynginx_v2 base/nginx:1.16.1   # 启动容器
 2 50a45a0894d8669308de7c70d47c96db8cd8990d3e34d1d125e5289ed062f126
 3 [root@docker01 ~]# 
 4 [root@docker01 ~]# docker ps 
 5 CONTAINER ID   IMAGE               COMMAND                  CREATED         STATUS         PORTS                NAMES
 6 50a45a0894d8   base/nginx:1.16.1   "sh entrypoint.sh ng…"   3 minutes ago   Up 3 minutes   0.0.0.0:80->80/tcp   mynginx_v2
 7 [root@docker01 ~]# netstat -lntup  # 端口查看
 8 Active Internet connections (only servers)
 9 Proto Recv-Q Send-Q Local Address           Foreign Address         State       PID/Program name    
10 tcp        0      0 127.0.0.1:25            0.0.0.0:*               LISTEN      1634/master         
11 tcp        0      0 0.0.0.0:111             0.0.0.0:*               LISTEN      1/systemd           
12 tcp        0      0 0.0.0.0:22              0.0.0.0:*               LISTEN      1349/sshd           
13 tcp6       0      0 ::1:25                  :::*                    LISTEN      1634/master         
14 tcp6       0      0 :::111                  :::*                    LISTEN      1/systemd           
15 tcp6       0      0 :::80                   :::*                    LISTEN      13625/docker-proxy  
16 tcp6       0      0 :::8080                 :::*                    LISTEN      2289/docker-proxy   
17 tcp6       0      0 :::22                   :::*                    LISTEN      1349/sshd           
18 udp        0      0 0.0.0.0:1021            0.0.0.0:*                           847/rpcbind         
19 udp        0      0 0.0.0.0:111             0.0.0.0:*                           1/systemd           
20 udp6       0      0 :::1021                 :::*                                847/rpcbind         
21 udp6       0      0 :::111                  :::*                                1/systemd

浏览器访问

http://172.16.1.31/

ZnYnqyJ.png!web

———END———

如果觉得不错就关注下呗 (-^O^-) !

FRZ3IvV.png!web


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK