版本比较

标识

  • 该行被添加。
  • 该行被删除。
  • 格式已经改变。

基础镜像的选择

(FROM)

基本原则

官方镜像优于非官方的镜像,如果没有官方镜像,则尽量选择Dockerfile开源的
  • 官方镜像优于非官方的镜像,如果没有官方镜像,则尽量选择Dockerfile开源的。
固定版本tag而不是每次都使用latest
  • 固定版本tag而不是每次都使用latest。
尽量选择体积小的镜像
  • 尽量选择体积小的镜像。
代码块
$ docker image ls
REPOSITORY      TAG             IMAGE ID       CREATED          SIZE
bitnami/nginx   1.18.0          dfe237636dde   28 minutes ago   89.3MB
nginx           1.21.0-alpine   a6eb2a334a9f   2 days ago       22.6MB
nginx           1.21.0          d1a364dc548d   2 days ago       133MB

Build一个Nginx镜像

假如我们有一个 
提示

alpine是一个体积非常小的Linux发行版,大小只有5MB,除了提供Linux必要的运行环境,基本不附带其他工具。

大部分受欢迎的镜像都会提供基于alpine制作的版本,比如python3.9.5-alpine。

示例:Build一个Nginx镜像

代码块
titleindex.html
 文件code
<h1>Hello Docker</h1>
代码块
准备一个Dockerfilecode
title
Dockerfile
FROM nginx:1.21.0-alpine

ADD index.html /usr/share/nginx/html/index.html

构建镜像:

代码块
docker image build -t mynginx-alpine .

延申阅读

通过 RUN 执行指令

RUN 主要用于在Image里执行指令,比如安装软件,下载文件等。

示例:

代码块
$ apt-get update
$ apt-get install wget
$ wget https://github.com/ipinfo/cli/releases/download/ipinfo-2.0.1/ipinfo_2.0.1_linux_amd64.tar.gz
$ tar zxf ipinfo_2.0.1_linux_amd64.tar.gz
$ mv ipinfo_2.0.1_linux_amd64 /usr/bin/ipinfo
$ rm -rf ipinfo_2.0.1_linux_amd64.tar.gz
Dockerfile

Dockerfile:

代码块
FROM ubuntu:21.04
RUN apt-get update
RUN apt-get install -y wget
RUN wget https://github.com/ipinfo/cli/releases/download/ipinfo-2.0.1/ipinfo_2.0.1_linux_amd64.tar.gz
RUN tar zxf ipinfo_2.0.1_linux_amd64.tar.gz
RUN mv ipinfo_2.0.1_linux_amd64 /usr/bin/ipinfo
RUN rm -rf ipinfo_2.0.1_linux_amd64.tar.gz
提示

以上方法不推荐,dockerfile里面的每个RUN指令都会生成一层image layer,导致镜像的臃肿。推荐将相关的命令都放到一个RUN指令里。

通过docker image history <image ID>查看镜像的分层:

代码块
镜像的大小和分层
$ docker image ls
REPOSITORY   TAG       IMAGE ID       CREATED         SIZE
ipinfo       latest    97bb429363fb   4 minutes ago   138MB
ubuntu       21.04     478aa0080b60   4 days ago      74.1MB
$ docker image history 97b
IMAGE          CREATED         CREATED BY                                      SIZE      COMMENT
97bb429363fb   4 minutes ago   RUN /bin/sh -c rm -rf ipinfo_2.0.1_linux_amd…   0B        buildkit.dockerfile.v0
<missing>
<missing>      4 minutes ago   RUN /bin/sh -c mv ipinfo_2.0.1_linux_amd64 /…   9.36MB    buildkit.dockerfile.v0
<missing>
<missing>      4 minutes ago   RUN /bin/sh -c tar zxf ipinfo_2.0.1_linux_am…   9.36MB    buildkit.dockerfile.v0
<missing>
<missing>      4 minutes ago   RUN /bin/sh -c wget https://github.com/ipinf…   4.85MB    buildkit.dockerfile.v0
<missing>
<missing>      4 minutes ago   RUN /bin/sh -c apt-get install -y wget # bui…   7.58MB    buildkit.dockerfile.v0
<missing>
<missing>      4 minutes ago   RUN /bin/sh -c apt-get update # buildkit        33MB      buildkit.dockerfile.v0
<missing>
<missing>      4 days ago      /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
<missing>
<missing>      4 days ago      /bin/sh -c mkdir -p /run/systemd && echo 'do…   7B
<missing>      4 days ago      /bin/sh -c [ -z "$(apt-get indextargets)" ]     0B
<missing>      4 days ago      /bin/sh -c set -xe   && echo '#!/bin/sh' > /…   811B
<missing>
<missing>      4 days ago      /bin/sh -c #(nop) ADD file:d6b6ba642344138dc…   74.1MB

每一行的RUN命令都会产生一层image layer, 导致镜像的臃肿。

改进版Dockerfile

改进版Dockerfile:

代码块
FROM ubuntu:21.04
RUN apt-get update && \
    apt-get install -y wget && \
    wget https://github.com/ipinfo/cli/releases/download/ipinfo-2.0.1/ipinfo_2.0.1_linux_amd64.tar.gz && \
    tar zxf ipinfo_2.0.1_linux_amd64.tar.gz && \
    mv ipinfo_2.0.1_linux_amd64 /usr/bin/ipinfo && \
    rm -rf ipinfo_2.0.1_linux_amd64.tar.gz

$ docker image ls
REPOSITORY   TAG       IMAGE ID       CREATED          SIZE
ipinfo-new   latest    fe551bc26b92   5 seconds ago    124MB
ipinfo       latest    97bb429363fb   16 minutes ago   138MB
ubuntu       21.04     478aa0080b60   4 days ago       74.1MB
$ docker image history fe5
IMAGE          CREATED          CREATED BY                                      SIZE      COMMENT
fe551bc26b92   16 seconds ago   RUN /bin/sh -c apt-get update &&     apt-get…   49.9MB    buildkit.dockerfile.v0
<missing>
<missing>      4 days ago       /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
<missing>
<missing>      4 days ago       /bin/sh -c mkdir -p /run/systemd && echo 'do…   7B
<missing>      4 days ago       /bin/sh -c [ -z "$(apt-get indextargets)" ]     0B
<missing>      4 days ago       /bin/sh -c set -xe   && echo '#!/bin/sh' > /…   811B
<missing>
<missing>      4 days ago       /bin/sh -c #(nop) ADD file:d6b6ba642344138dc…   74.1MB
$
文件复制和目录操作 (ADD,COPY,WORKDIR)
提示

尽量将RUN指令都写到一行里。

文件复制和目录操作

往镜像里复制文件有两种方式,COPY  ADD , 我们来看一下两者的不同。

复制普通文件

COPY

 

 ADD 都可以把local的一个文件复制到镜像里,如果目标目录不存在,则会自动创建

ADD都可以把一个本地的文件复制到镜像里,如果目标目录不存在,则会自动创建。比如把本地的 hello.py 复制到 /app 目录下,如果 /app这个路径不存在,则会自动创建。

代码块
FROM python:3.9.5-alpine3.13
COPY hello.py /app/
hello.py比如把本地的
hello.py
复制到 /app 目录下。 /app这个folder不存在,则会自动创建

复制压缩文件

ADD

 

COPY高级一点的地方就是,如果复制的是一个gzip等压缩文件时,ADD会帮助我们自动去解压缩文件。

COPY高级一点的地方就是,如果复制的是一个gzip等压缩文件时,ADD会帮助我们自动去解压缩文件。

代码块
FROM python:3.9.5-alpine3.13
ADD hello.tar.gz /app/
如何选择
提示
因此在

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

ADD。

ADD

WORKDIR

用于指定工作路径,当路径不存在时会自动创建,指定工作路径后,后续的操作都在该路径下进行:

代码块
FROM python:3.9.5-alpine3.13
WORKDIR /app
COPY hello.py hello.py

构建参数和环境变量 (ARG vs ENV)

ARG  ENV 是经常容易被混淆的两个Dockerfile的语法,都可以用来设置一个“变量”。 但实际上两者有很多的不同。

代码块
FROM ubuntu:21.04
RUN apt-get update && \
    apt-get install -y wget && \
    wget https://github.com/ipinfo/cli/releases/download/ipinfo-2.0.1/ipinfo_2.0.1_linux_amd64.tar.gz && \
    tar zxf ipinfo_2.0.1_linux_amd64.tar.gz && \
    mv ipinfo_2.0.1_linux_amd64 /usr/bin/ipinfo && \
    rm -rf ipinfo_2.0.1_linux_amd64.tar.gz
ENV

ENV:

代码块
FROM ubuntu:21.04
ENV VERSION=2.0.1
RUN apt-get update && \
    apt-get install -y wget && \
    wget https://github.com/ipinfo/cli/releases/download/ipinfo-${VERSION}/ipinfo_${VERSION}_linux_amd64.tar.gz && \
    tar zxf ipinfo_${VERSION}_linux_amd64.tar.gz && \
    mv ipinfo_${VERSION}_linux_amd64 /usr/bin/ipinfo && \
    rm -rf ipinfo_${VERSION}_linux_amd64.tar.gz
ARG

ARG:

代码块
FROM ubuntu:21.04
ARG VERSION=2.0.1
RUN apt-get update && \
    apt-get install -y wget && \
    wget https://github.com/ipinfo/cli/releases/download/ipinfo-${VERSION}/ipinfo_${VERSION}_linux_amd64.tar.gz && \
    tar zxf ipinfo_${VERSION}_linux_amd64.tar.gz && \
    mv ipinfo_${VERSION}_linux_amd64 /usr/bin/ipinfo && \
    rm -rf ipinfo_${VERSION}_linux_amd64.tar.gz
区别

区别:

Image Modified

提示

ARG

可以在镜像build的时候动态修改value, 通过 

声明的变量只在镜像构建阶段起变量替换作用,对容器没有影响,而ENV则除了在构建阶段起作用,还会影响到创建的容器,在创建的容器里,含有ENV指定的环境变量(通过env命令可查看)。

除此之外,ARG声明的变量可以在构建的时候动态修改,使用--build-arg参数,以实现在同一个Dockerfile上通过不同的构建命令来构建不同镜像的功能。

代码块
$ docker image build -f .\
Dockerfile
Dockerfile-arg -t ipinfo-arg-2.0.0 --build-arg VERSION=2.0.0 .
$ docker image ls
REPOSITORY         TAG       IMAGE ID       CREATED          SIZE
ipinfo-arg-2.0.0   latest    0d9c964947e2   6 seconds ago    124MB
$ docker container run -it ipinfo-arg-2.0.0
root@b64285579756:/#
root@b64285579756:/# ipinfo version
2.0.0
root@b64285579756:/#
ENV 设置的变量可以在Image中保持,并在容器中的环境变量里

容器启动命令 CMD

CMD可以用来设置容器启动时默认会执行的命令。

  • 容器启动时默认执行的命令
  • 如果docker container run启动容器时指定了其它命令,则CMD命令会被忽略
    代码块
    CMD ["可执行参数", "参数1", "参数2", ...]
    • 容器启动时默认执行的命令。
    • 如果docker container run启动容器时指定了其它命令(跟在命令行最后),则CMD命令会被忽略。
    • 如果定义了多个CMD,只有最后一个会被执行。
    代码块
    FROM ubuntu:21.04
    ENV VERSION=2.0.1
    RUN apt-get update && \
        apt-get install -y wget && \
        wget https://github.com/ipinfo/cli/releases/download/ipinfo-${VERSION}/ipinfo_${VERSION}_linux_amd64.tar.gz && \
        tar zxf ipinfo_${VERSION}_linux_amd64.tar.gz && \
        mv ipinfo_${VERSION}_linux_amd64 /usr/bin/ipinfo && \
        rm -rf ipinfo_${VERSION}_linux_amd64.tar.gz
    
    $ docker image build -t ipinfo .
    $ docker container run -it ipinfo
    root@8cea7e5e8da8:/#
    root@8cea7e5e8da8:/#
    root@8cea7e5e8da8:/#
    root@8cea7e5e8da8:/# pwd
    /
    root@8cea7e5e8da8:/#


    默认进入到shell是因为在ubuntu的基础镜像里有定义CMD。

    默认进入到shell是因为在ubuntu的基础镜像里有定义CMD
    代码块
    $docker image history ipinfo
    IMAGE          CREATED        CREATED BY                                      SIZE      COMMENT
    db75bff5e3ad   24 hours ago   RUN /bin/sh -c apt-get update &&     apt-get…   50MB      buildkit.dockerfile.v0
    
    <missing>
    <missing>      24 hours ago   ENV VERSION=2.0.1                               0B        buildkit.dockerfile.v0
    
    <missing>
    <missing>      7 days ago     /bin/sh -c #(nop)  CMD ["/bin/bash"]            0B
    
    <missing>
    <missing>      7 days ago     /bin/sh -c mkdir -p /run/systemd && echo 'do…   7B
    <missing>      7 days ago     /bin/sh -c [ -z "$(apt-get indextargets)" ]     0B
    <missing>      7 days ago     /bin/sh -c set -xe   && echo '#!/bin/sh' > /…   811B
    
    <missing>
    <missing>      7 days ago     /bin/sh -c #(nop) ADD file:d6b6ba642344138dc…   74.1MB

    容器启动命令 ENTRYPOINT

    ENTRYPOINT

    也可以设置容器启动时要执行的命令,但是和CMD是有区别的。

    也可以设置容器启动时要执行的命令,但是和CMD是有区别的。

    • CMD 设置的命令,可以在docker container run 时传入其它命令,覆盖掉 CMD 的命令,但是 ENTRYPOINT 所设置的命令是一定会被执行的。
    • ENTRYPOINT  CMD 可以联合使用,ENTRYPOINT 
    设置执行的命令,CMD传递参数
    • 设置执行的命令,CMD传递参数。


    代码块
    FROM ubuntu:21.04
    CMD ["echo", "hello docker"]

    把上面的Dockerfile build成一个叫 demo-cmd 

    的镜象

    的镜象:

    代码块
    $ docker image ls
    REPOSITORY        TAG       IMAGE ID       CREATED      SIZE
    demo-cmd          latest    5bb63bb9b365   8 days ago   74.1MB
    代码块
    FROM ubuntu:21.04
    ENTRYPOINT ["echo", "hello docker"]

    build成一个叫 demo-entrypoint 

    的镜像

    的镜像:

    代码块
    $ docker image ls
    REPOSITORY        TAG       IMAGE ID       CREATED      SIZE
    demo-entrypoint   latest    b1693a62d67a   8 days ago   74.1MB


    CMD的镜像,如果执行创建容器,不指定运行时的命令,则会默认执行CMD所定义的命令,打印出hello

    docker

    docker:

    代码块
    $ docker container run -it --rm demo-cmd
    hello docker

    但是如果我们docker container run的时候指定命令,则该命令会覆盖掉CMD的命令,如:

    代码块
    $ docker container run -it --rm demo-cmd echo "hello world"
    hello world

    但是ENTRYPOINT的容器里ENTRYPOINT所定义的命令则无法覆盖,一定会执行:

    但是ENTRYPOINT的容器里ENTRYPOINT所定义的命令则无法覆盖,一定会执行
    代码块
    $ docker container run -it --rm demo-entrypoint
    hello docker
    $ docker container run -it --rm demo-entrypoint echo "hello world"
    hello docker echo hello world
    $

    Shell 格式和 Exec 格式

    CMD和ENTRYPOINT同时支持shell格式和Exec格式。

    Shell格式:

    Shell格式
    代码块
    CMD echo "hello docker"
    
    ENTRYPOINT echo "hello docker"
    Exec格式

    Exec格式:

    以可执行命令的方式
    代码块
    ENTRYPOINT ["echo", "hello docker"]
    
    CMD ["echo", "hello docker"]

    注意shell脚本的问题:

    注意shell脚本的问题
    代码块
    FROM ubuntu:21.04
    ENV NAME=docker
    CMD echo "hello $NAME"

    假如我们要把上面的CMD改成Exec格式,下面这样改是不行的, 大家可以试试。

    代码块
    FROM ubuntu:21.04
    ENV NAME=docker
    CMD ["echo", "hello $NAME"]

    它会打印出 hello $NAME , 而不是 hello docker ,那么需要怎么写呢?

    我们需要以shell脚本的方式去执行

    我们需要以shell脚本的方式去执行:

    代码块
    FROM ubuntu:21.04
    ENV NAME=docker
    CMD ["sh", "-c", "echo hello $NAME"]

    一起构建一个 Python Flask 镜像

    Python程序:

    Python 程序
    代码块
    from flask import Flask
    
    app = Flask(__name__)
    
    
    @app
    @app.route('/'
    )
    )
    def hello_world():
        return 'Hello, World!'

    Dockerfile:

    代码块
    FROM python:3.9.5-slim
    
    COPY app.py /src/app.py
    
    RUN pip install flask
    
    WORKDIR /src
    ENV FLASK_APP=app.py
    
    EXPOSE 5000
    
    CMD ["flask", "run", "-h", "0.0.0.0"]
    提示

    EXPOSE 5000表示暴露容器的5000端口。

    构建运行:

    代码块
    docker image build -t flask-demo .
    docker container run -p 5000:5000 flask-demo

    Dockerfile 技巧——合理使用缓存

    第一次构建镜像时,docker会为各层镜像生成缓存,后续build时,docker会尽量使用缓存来构建,以加快构建速度。

    Dockerfile的每条指令对应一层镜像,如果某一层有修改,则这层和后续所有的层都无法使用缓存来加速构建。

    为了合理使用缓存,应该将容易变化的层安装到后面,而将不容量变化的层放到前面,以尽可能使用缓存来构建

    示例:

    def hello_world(): return 'Hello, World!'

    Dockerfile

    代码块
    FROM python:3.9.5-slim
    COPY app.py /src/app.py
    
    
    RUN pip install flask
    
    WORKDIR /src
    ENV FLASK_APP=app.py
    
    COPY app.py /src/app.py
    
    EXPOSE 5000
    
    CMD ["flask", "run", "-h", "0.0.0.0"]

    Dockerfile

    技巧——合理使用缓存

    Dockerfile 技巧——合理使用 .dockerignore

    什么是Docker build context

    Docker是client-server架构,理论上Client和Server可以不在一台机器上。

    在构建docker镜像的时候,需要把所需要的文件由CLI(client)发给Server,这些文件实际上就是build

    context

    context。

    举例:

    代码块
    $ dockerfile-demo more Dockerfile
    FROM python:3.9.5-slim
    
    RUN pip install flask
    
    WORKDIR /src
    ENV FLASK_APP=app.py
    
    COPY app.py /src/app.py
    
    EXPOSE 5000
    
    CMD ["flask", "run", "-h", "0.0.0.0"]
    $ dockerfile-demo more app.py
    from flask import Flask
    
    app = Flask(__name__)
    
    
    @app.route('/')
    def hello_world():
        return 'Hello, world!'

    构建的时候,第一行输出就是发送build context。11.13MB

    (这里是Linux环境下的log)

    (这里是Linux环境下的log)。

    代码块
    $ docker image build -t demo .
    Sending build context to Docker daemon  11.13MB
    Step 1/7 : FROM python:3.9.5-slim
     ---> 609da079b03a
    Step 2/7 : RUN pip install flask
     ---> Using cache
     ---> 955ce495635e
    Step 3/7 : WORKDIR /src
     ---> Using cache
     ---> 1c2f968e9f9b
    Step 4/7 : ENV FLASK_APP=app.py
     ---> Using cache
     ---> dceb15b338cf
    Step 5/7 : COPY app.py /src/app.py
     ---> Using cache
     ---> 0d4dfef28b5f
    Step 6/7 : EXPOSE 5000
     ---> Using cache
     ---> 203e9865f0d9
    Step 7/7 : CMD ["flask", "run", "-h", "0.0.0.0"]
     ---> Using cache
     ---> 35b5efae1293
    Successfully built 35b5efae1293
    Successfully tagged demo:latest

    . 这个参数就是代表了build

    context所指向的目录

    context所指向的目录。

    .dockerignore 文件

    代码块
    .vscode/
    env/

    有了.dockerignore文件后,我们再build, build context就小了很多,4.

    096kB

    096kB。

    代码块
    $ docker image build -t demo .
    Sending build context to Docker daemon  4.096kB
    Step 1/7 : FROM python:3.9.5-slim
    ---> 609da079b03a
    Step 2/7 : RUN pip install flask
    ---> Using cache
    ---> 955ce495635e
    Step 3/7 : WORKDIR /src
    ---> Using cache
    ---> 1c2f968e9f9b
    Step 4/7 : ENV FLASK_APP=app.py
    ---> Using cache
    ---> dceb15b338cf
    Step 5/7 : COPY . /src/
    ---> a9a8f888fef3
    Step 6/7 : EXPOSE 5000
    ---> Running in c71f34d32009
    Removing intermediate container c71f34d32009
    ---> fed6995d5a83
    Step 7/7 : CMD ["flask", "run", "-h", "0.0.0.0"]
    ---> Running in 7ea669f59d5e
    Removing intermediate container 7ea669f59d5e
    ---> 079bae887a47
    Successfully built 079bae887a47
    Successfully tagged demo:latest

    Dockerfile 技巧——镜像的多阶段构建

    提示
    这一节来聊聊多阶段构建,以及为什么要使用它。

    多阶段构建适用于编译型语言,比如C,Go。构建时,第一阶段是安装编译所需的环境,以生成可执行程序,这个编译环境一般比较大,比如完整的gcc编译环境或Go编译环境。由于生成可执行文件之后编译环境就不需要了,所以可以在第二阶段选择一个比较小的运行环境,只要能够支持运行可执行程序即可。

    C语言例子

    假如有一个C的程序,我们想用Docker去做编译,然后执行可执行文件。

    #include <stdio.h>

    代码块
    titlehello.c
    #include <stdio.h>
    
    void main(int argc, char *argv[])
    {
        printf("hello %s\n", argv[argc - 1]);
    }

    本地测试:

    本地测试(如果你本地有C环境)
    代码块
    $ gcc --static -o hello hello.c
    $ ls
    hello  hello.c
    $ ./hello docker
    hello docker
    $ ./hello world
    hello world
    $ ./hello friends
    hello friends
    $
    构建一个Docker镜像,因为要有C的环境,所以我们选择gcc这个image

    构建一个Docker镜像,因为要有C的环境,所以我们选择gcc这个image:

    代码块
    FROM gcc:9.4
    
    COPY hello.c /src/hello.c
    
    WORKDIR /src
    
    RUN gcc --static -o hello hello.c
    
    ENTRYPOINT [ "/src/hello" ]
    
    CMD []
    build和测试

    build和测试:

    代码块
    $ docker build -t hello .
    Sending build context to Docker daemon   5.12kB
    Step 1/6 : FROM gcc:9.4
    ---> be1d0d9ce039
    Step 2/6 : COPY hello.c /src/hello.c
    ---> Using cache
    ---> 70a624e3749b
    Step 3/6 : WORKDIR /src
    ---> Using cache
    ---> 24e248c6b27c
    Step 4/6 : RUN gcc --static -o hello hello.c
    ---> Using cache
    ---> db8ae7b42aff
    Step 5/6 : ENTRYPOINT [ "/src/hello" ]
    ---> Using cache
    ---> 7f307354ee45
    Step 6/6 : CMD []
    ---> Using cache
    ---> 7cfa0cbe4e2a
    Successfully built 7cfa0cbe4e2a
    Successfully tagged hello:latest
    $ docker image ls
    REPOSITORY     TAG          IMAGE ID       CREATED       SIZE
    hello          latest       7cfa0cbe4e2a   2 hours ago   1.14GB
    gcc            9.4          be1d0d9ce039   9 days ago    1.14GB
    $ docker run --rm -it hello docker
    hello docker
    $ docker run --rm -it hello world
    hello world
    $ docker run --rm -it hello friends
    hello friends
    $

    可以看到镜像非常的大,1.

    14GB

    14GB。

    实际上当我们把hello.c编译完以后,并不需要这样一个大的GCC环境,一个小的alpine镜像就可以了。

    这时候我们就可以使用多阶段构建了。

    代码块
    FROM gcc:9.4 AS builder
    
    COPY hello.c /src/hello.c
    
    WORKDIR /src
    
    RUN gcc --static -o hello hello.c
    
    
    FROM alpine:3.13.5
    
    COPY --from=builder /src/hello /src/hello
    
    ENTRYPOINT [ "/src/hello" ]
    
    CMD []
    测试

    测试:

    代码块
    $ docker build -t hello-apline -f Dockerfile-new .
    Sending build context to Docker daemon   5.12kB
    Step 1/8 : FROM gcc:9.4 AS builder
    ---> be1d0d9ce039
    Step 2/8 : COPY hello.c /src/hello.c
    ---> Using cache
    ---> 70a624e3749b
    Step 3/8 : WORKDIR /src
    ---> Using cache
    ---> 24e248c6b27c
    Step 4/8 : RUN gcc --static -o hello hello.c
    ---> Using cache
    ---> db8ae7b42aff
    Step 5/8 : FROM alpine:3.13.5
    ---> 6dbb9cc54074
    Step 6/8 : COPY --from=builder /src/hello /src/hello
    ---> Using cache
    ---> 18c2bce629fb
    Step 7/8 : ENTRYPOINT [ "/src/hello" ]
    ---> Using cache
    ---> 8dfb9d9d6010
    Step 8/8 : CMD []
    ---> Using cache
    ---> 446baf852214
    Successfully built 446baf852214
    Successfully tagged hello-apline:latest
    $ docker image ls
    REPOSITORY     TAG          IMAGE ID       CREATED       SIZE
    hello-alpine   latest       446baf852214   2 hours ago   6.55MB
    hello          latest       7cfa0cbe4e2a   2 hours ago   1.14GB
    demo           latest       079bae887a47   2 hours ago   125MB
    gcc            9.4          be1d0d9ce039   9 days ago    1.14GB
    $ docker run --rm -it hello-alpine docker
    hello docker
    $ docker run --rm -it hello-alpine world
    hello world
    $ docker run --rm -it hello-alpine friends
    hello friends
    $

    可以看到这个镜像非常小,只有6.

    55MB

    55MB。

    Go语言例子

    Angular例子

    Dockerfile 技巧——尽量使用非root用户

    Note
    提示

    本节课需要一个Linux的Docker环境。

    Root的危险性

    docker的root权限一直是其遭受诟病的地方,docker的root权限有那么危险么?我们举个例子。

    假如我们有一个用户,叫demo,它本身不具有sudo的权限,所以就有很多文件无法进行读写操作,比如/root目录它是无法查看的。

    代码块
    [demo@docker-host ~]$ sudo ls /root
    [sudo] password for demo:
    demo is not in the sudoers file.  This incident will be reported.
    [demo@docker-host ~]$

    但是这个用户有执行docker的权限,也就是它在docker这个group里。

    代码块
    [demo@docker-host ~]$ groups
    demo docker
    [demo@docker-host ~]$ docker image ls
    REPOSITORY   TAG       IMAGE ID       CREATED      SIZE
    busybox      latest    a9d583973f65   2 days ago   1.23MB
    [demo@docker-host ~]$

    这时,我们就可以通过Docker做很多越权的事情了,比如,我们可以把这个无法查看的/root目录映射到docker container里,你就可以自由进行查看了。

    代码块
    [demo@docker-host vagrant]$ docker run -it -v /root/:/root/tmp busybox sh
    / # cd /root/tmp
    ~/tmp # ls
    anaconda-ks.cfg  original-ks.cfg
    ~/tmp # ls -l
    total 16
    -rw-------    1 root     root          5570 Apr 30  2020 anaconda-ks.cfg
    -rw-------    1 root     root          5300 Apr 30  2020 original-ks.cfg
    ~/tmp #

    更甚至我们可以给我们自己加sudo权限。我们现在没有sudo权限。

    更甚至我们可以给我们自己加sudo权限。我们现在没有sudo权限
    代码块
    [demo@docker-host ~]$ sudo vim /etc/sudoers
    [sudo] password for demo:
    demo is not in the sudoers file.  This incident will be reported.
    [demo@docker-host ~]$

    但是我可以给自己添加。

    代码块
    [demo@docker-host ~]$ docker run -it -v /etc/sudoers:/root/sudoers busybox sh
    / # echo "demo    ALL=(ALL)       ALL" >> /root/sudoers
    / # more /root/sudoers | grep demo
    demo    ALL=(ALL)       ALL

    然后退出container,bingo,我们有sudo权限了。

    代码块
    [demo@docker-host ~]$ sudo more /etc/sudoers | grep demo
    demo    ALL=(ALL)       ALL
    [demo@docker-host ~]$

    如何使用非root用户

    我们准备两个Dockerfile,第一个Dockerfile如下,其中app.

    py文件源码请参考 :ref:`一起构建一个 Python Flask 镜像` 

    py文件源码前面flask的示例一样:

    代码块
    FROM python:3.9.5-slim
    
    RUN pip install flask
    
    COPY app.py /src/app.py
    
    WORKDIR /src
    ENV FLASK_APP=app.py
    
    EXPOSE 5000
    
    CMD ["flask", "run", "-h", "0.0.0.0"]

    假设构建的镜像名字为 flask-demo

    第二个Dockerfile,使用非root用户来构建这个镜像,名字叫 flask-no-root Dockerfile如下:

    • 通过groupadd和useradd创建一个flask的组和用户
    • 通过USER指定后面的命令要以flask这个用户的身份运行
    代码块
    FROM python:3.9.5-slim
    
    RUN pip install flask && \
        groupadd -r flask && useradd -r -g flask flask && \
        mkdir /src && \
        chown -R flask:flask /src
    
    USER flask
    
    COPY app.py /src/app.py
    
    WORKDIR /src
    ENV FLASK_APP=app.py
    
    EXPOSE 5000
    
    CMD ["flask", "run", "-h", "0.0.0.0"]
    
    $ docker image ls
    REPOSITORY      TAG          IMAGE ID       CREATED          SIZE
    flask-no-root   latest       80996843356e   41 minutes ago   126MB
    flask-demo      latest       2696c68b51ce   49 minutes ago   125MB
    python          3.9.5-slim   609da079b03a   2 weeks ago      115MB

    分别使用这两个镜像创建两个容器:

    分别使用这两个镜像创建两个容器
    代码块
    $ docker run -d --name flask-root flask-demo
    b31588bae216951e7981ce14290d74d377eef477f71e1506b17ee505d7994774
    $ docker run -d --name flask-no-root flask-no-root
    83aaa4a116608ec98afff2a142392119b7efe53617db213e8c7276ab0ae0aaa0
    $ docker container ps
    CONTAINER ID   IMAGE           COMMAND                  CREATED          STATUS          PORTS      NAMES
    83aaa4a11660   flask-no-root   "flask run -h 0.0.0.0"   4 seconds ago    Up 3 seconds    5000/tcp   flask-no-root
    b31588bae216   flask-demo      "flask run -h 0.0.0.0"   16 seconds ago   Up 15 seconds   5000/tcp   flask-root

    目录
    maxLevel1