跳到主要内容

docker构建镜像

构建镜像

镜像是多层存储,每一层在前一层的基础上进行的修改;而容器也是多层存储,是在以镜像为基础层,在其上再增加一层作为容器运行时的存储层。

命令速查

docker images 列出所有镜像

docker system df 查看镜像占用的磁盘大小

docker history 镜像 查看镜像历史记录(提交的层数,执行的命令等)--no-trunc 避免命令被截断

docker diff 容器 查看容器相对于镜像,有哪些改动

如何查看镜像大小

docker images会列出所有镜像,后面的size是这个镜像分层存储后,所有层加起来的大小,因为层可能共用,所以实际占用的磁盘大小可能要比这里的size加起来小。

docker system df 可以查看镜像、容器、数据卷所占用的实际磁盘大小。

如何查看一个镜像的dockerfile

如果是dockerhub上下载的镜像,可以在镜像页面看是否有Dockerfile标记,点进去一般能看到,但并不是所有镜像都会公布自己的Dockerfile。

另外,还可以使用docker history --no-trunc image_name_or_id来查看镜像构建过程中运行的所有命令,虽然不完全等同于Dockerfile,但是可以找到所有关键内容。

还有个第三方工具,CenturyLinkLabs的dockerfile-from-image,可以用来从镜像生成Dockerfile,没用过,不了解具体功能GitHub - CenturyLinkLabs/dockerfile-from-image

docker commit(理解如何构建)

启动一个容器后,可以通过docker exec -it <container-id> bash进入容器,修改容器里的内容。如果希望把修改保存下来,可以通过

docker commit [选项] <容器ID或容器名> [<仓库名>[:<标签>]]把容器保存为镜像。

docker commit 相当于是把基于镜像增加了一层存储层的容器,整体保存成了新的镜像。

通过docker history <image>:<tag>可以查看当前镜像有多少层,通过commit保存的镜像,会比原始镜像多一层。

docker commit可以比较直观的帮助理解镜像分层存储的概念,但是实际环境中,不建议使用docker commit来创建镜像,原因有2:

  • 联动修改容易臃肿:在容器中执行操作时,改造某个内容,会联动造成很多文件被修改,如果不小心的清理,会造成镜像比较臃肿

  • 黑盒难维护:用docker commit生成的镜像,对镜像的操作都是黑箱操作,除了制作的人,其他人都不知道做了什么,后期难以维护

查看容器里改变的内容

通过docker exec -it container_name_or_id bash进入容器后,可以执行一些动作,然后退出后,可以通过docker diff container_name_or_id来查看容器里的文件发生了哪些改变。

docker diff 容器,其实就是查看容器基于镜像增加的存储层都有哪些内容。

Dockerfile(正常构建方式)

镜像的定制,实际就是定制每一层所添加的配置、文件,把每一层的修改、安装、构建、操作的命令写入一个脚本,用这个脚本来构建、定制镜像,上面提到的无法重复、镜像构建黑盒、体积的问题就都解决了,这个脚本就是Dockerfile。

Dockerfile 是一个文本文件,其内包含了一条条的 指令(Instruction),每一条指令构建一层,因此每一条指令的内容,就是描述该层应当如何构建。

FROM 指定基础镜像

  • 服务类镜像 如nginx、redis等等

  • 语言类镜像 如node、python、golang等

  • 操作系统类镜像 如ubuntu、alpine等

  • 特殊镜像 FROM scratch,这是一个虚拟镜像,并不存在,表示空白镜像

    对linux下静态编译的程序来说,不需要有操作系统提供运行时支持,所需一切库都在可执行文件里了,因此直接从空白镜像构建会让体积更小巧,比如使用go开发的应用,很多会用这种方式来制作镜像。

RUN 执行命令

执行命令行命令,有两种方式,每行RUN命令,都会创建一个新的层

  • RUN [命令]

  • RUN ["脚本文件","param1","param2"]

如果要执行多条命令

#最好不要每个命令写一行
RUN <command1>
RUN <command2>
#相同目的的内容,应该写成下面这样,只生成一层,并且在最后清理掉多余的文件,避免臃肿
# 镜像是多层存储,每一层的东西并不会在下一层被删除,会一直跟随着镜像。因此镜像构建时,
# 一定要确保每一层只添加真正需要添加的东西,任何无关的东西都应该清理掉。
RUN <command1> \
&& <command2>
&& <清理无关内容的命令>

构建和查看镜像

docker build -t getting-started .

构建镜像

-t flag tags our image

. docker should look for the Dockerfile in the current directory

docker tag getting-started:latest getting-started:v1.0.0

为一个镜像创建一个新tag,而不是生成一个新镜像,两个tag指向同一个镜像

docker rmi getting-started:v1.0.0 rmi是删除镜像,但是本例中,是移除了v1.0.0的tag,并没有删除镜像本身。

docker images

列出所有镜像

docker image history getting-started

查看镜像的层,可以看到各层的大小,帮助诊断镜像大小。

增加--no-trunc选项可以查看被截断的文字全文

一些image building的最佳实践

docker scan getting-started

扫描getting-started是否存在安全问题(远程的)

镜像是分层的,一旦某个层有变化,所有依赖他的下游的层,都要重新创建

所以,多步骤构建非常必要,尽量保证改动在下游,其他不变的东西预先搞好,构建镜像就会比较快。

分离构建依赖和运行依赖,只包含我们要用的东西,不要其他多余的东西,缩减镜像尺寸。

构建上下文

docker在运行时分为docker引擎(即服务端守护进程)和客户端工具,docker命令就是客户端工具。引擎提供一组REST API(Docker Remote API),docker命令这样的客户端工具,通过这组api与引擎交互完成功能。表面上我们在本机执行docker命令,实际上都是通过api在docker引擎里完成动作。

docker build -t <image>:<tag> .

命令里的.非常重要,他指定的是构建上下文,docker build命令,会把该目录下的内容打包发给docker引擎以构建镜像,这时候很多像copy这类命令中的源文件路径,都是相对于这个路径的相对路径,且因为是打包发送到引擎的,只能是该目录本身及其子目录。

所以COPY /opt/xxx /app这类命令无法工作,是因为打包的上下文下,并没有/opt目录,只能使用如COPY ./xxx /app里的./xxx这样的相对路径

比如有些初学者在发现 COPY /opt/xxxx /app 不工作后,于是干脆将 Dockerfile 放到了硬盘根目录去构建,结果发现 docker build 执行后,在发送一个几十 GB 的东西,极为缓慢而且很容易构建失败。那是因为这种做法是在让 docker build 打包整个硬盘,这显然是使用错误。

一般来说,应该会将 Dockerfile 置于一个空目录下,或者项目根目录下。如果该目录下没有所需文件,那么应该把所需文件复制一份过来。如果目录下有些东西确实不希望构建时传给 Docker 引擎,那么可以用 .gitignore 一样的语法写一个 .dockerignore,该文件是用于剔除不需要作为上下文传递给 Docker 引擎的。

默认情况下,如果不额外指定Dockerfile,会将上下文目录下的名为Dockerfile的文件作为Dockerfile,实际上并不是必须名为Dockerfile,而且并不是必须位于上下文目录里,可以用-f ../xxx/xx/aa.txt这样的方式来指定某个文件作为Dockerfile。大家只是习惯使用默认的文件名Dockerfile,并且习惯性将其放在镜像构建的上下文目录里。

Dockerfile指令

参考:https://yeasy.gitbook.io/docker_practice/image/dockerfile

CMD

指定容器启动程序和参数

和RUN相似,也是两种格式

  • CMD <command>

  • CMD ["exec_file", "param1", "param2"...]

Docker不是虚拟机,容器就是进程,既然是进程,那么在容器启动时,需要指定所运行的程序及参数

运行时可以指定新的命令来替代镜像中的这个默认命令。

ubuntu镜像中默认的CMD是/bin/bashdocker run -it ubuntu cat /etc/os-release就是用cat /etc/os-release命令替换了默认的/bin/bash命令。

直接在docker run的最后写上新的命令

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

比如CMD service nginx start,尝试以后台守护进程形式启动nginx服务,然而这行命令会被理解为CMD [ "sh", "-c", "service nginx start"],相当于容器的进程是sh,那么当主进程sh运行完成之后,容器就会退出。

ENTRYPOINT

入口点,格式和RUN执行格式一样,分为exec和shell两种格式。

作用和CMD一样,也是指定容器启动程序和参数

和CMD一样,在docker run的时候,也可以被替代,通过--entrypoint

当指定了ENTRYPOINT之后,CMD就不再是容器启动程序,而是变成了ENTRYPOINT的参数:

<ENTRYPOINT> "<CMD>"

有了CMD,为什么还需要ENTRYPOINT呢:

  • 场景1:把镜像当做命令一样使用

    FROM ubuntu:18.04
    RUN apt-get update \
    && apt-get install -y curl \
    && rm -rf /var/lib/apt/lists/*
    ENTRYPOINT [ "curl", "-s", "http://myip.ipip.net" ]

    docker build -t myip .构建myip镜像,然后运行docker run myip就可以获取当前ip,这时候如果在后面再加上参数-i,即docker run myip -i,那-i就是CMD,此时会作为参数传给ENTRYPOINT,这样curl命令就收到了-i参数,可以打印请求header了。

  • 场景2:容器运行前准备工作

    启动容器就是启动主进程,有时候,启动前,需要做一些准备工作,这些准备工作就可以放在ENTRYPOINT里来做,比如写一个脚本放到ENTRYPOINT里,然后这个脚本会接收CMD作为参数,在脚本的最后,执行传来的CMD的内容。

    以redis镜像为例:

    FROM alpine:3.4
    ...
    RUN addgroup -S redis && adduser -S -G redis redis
    ...
    ENTRYPOINT ["docker-entrypoint.sh"]


    EXPOSE 6379
    CMD [ "redis-server" ]

    docker-entrypoint.sh

    #!/bin/sh
    ...
    # allow the container to be started with `--user`
    if [ "$1" = 'redis-server' -a "$(id -u)" = '0' ]; then
    find . \! -user redis -exec chown redis '{}' +
    exec gosu redis "$0" "$@"
    fi

    exec "$@"

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

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

WORKDIR

指定工作目录 WORKDIR <工作目录路径>

指定工作目录之后,以后各层的当前目录就被改为指定的目录,如果目录不存在,会被创建。

WORKDIR /a
WORKDIR b
WORKDIR c

RUN pwd

RUN pwd这层的工作目录是/a/b/c

Dockerfile多阶段构建

https://yeasy.gitbook.io/docker_practice/image/multistage-builds#gou-jian-shi-cong-qi-ta-jing-xiang-fu-zhi-wen-jian

阶段

为某一个阶段命名, 如FROM golang:alpine as builder

如果只想构建builder阶段的镜像时,增加--target=builder参数即可

复制文件

  • 从上一阶段的镜像中复制文件

COPY --from=0 /go/src/github.com/go/helloworld/app .

  • 从任意镜像中复制文件

COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf