第四章-Dockerfile完全指南

  1. 如何选择基础镜像
    基本原则:
  • 官方镜像优于非官方镜像,如果没有官方镜像,则尽量选择Dockerfile开源的;
  • 固定版本tag,而不是每次都使用最新版本latest
  • 尽量选择体积小的镜像
  1. build一个nginx镜像
    Dockerfile文件:
FROM nginx:stable
ADD index.html /usr/share/nginx/html/index.html

index.html文件:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
        <h1>哈哈哈,这是我的第一个Dockerfile创建的镜像</>
</body>
</html>

使用Dockerfile构建镜像
sudo docker image build -t 1341935531/index-nginx:2.0 . 通过新建的镜像创建并运行容器
sudo docker run -d -p 80:80 --name index_nginx image_id

通过RUN执行指令

  1. RUN主要用于在image里执行指令,比如安装软件、下载文件等,例如
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 -xzvf 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生成镜像文件

sudo docker image build -t run_dockerfile:1.0 .

查看镜像和镜像对应的分层

dockerfile 基础镜像 dockerfile基础镜像选型_Dockerfile


可以看到,每个RUN命令,都给镜像增加了一层,使得镜像非常的臃肿,所以我们需要优化生成镜像的命令,

  1. 将多个RUN命令改为一个RUN命令
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 -xzvf 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.good创建镜像

sudo docker image build -t run_dockerfile:2.0 -f Dockerfile.good .

在查看新创建的镜像的分层,明显可以看出,比之前层数少了很多,而且size也变小了

dockerfile 基础镜像 dockerfile基础镜像选型_dockerfile 基础镜像_02

文件的复制和目录操作

  1. 往镜像里复制文件有两种方式,ADD和COPY,我们来看一下两者的不同
    COPY和ADD都可以把local的一个文件复制到镜像里,如果目标目录不存在,则会自动创建
  2. COPY复制普通文件
FROM python:3.9.13-alpine3.14
COPY hello.py /app/
CMD ["python", "/app/hello.py"]

比如把本地的hello.py复制到/app/目录下,如果/app/这个目录不存在,则会自动创建
注意: 不能这样写/app, 而是要这样写/app/,否则会报错,找不到
3. ADD复制压缩文件
ADD比COPY高级一点的地方就是,如果复制的是一个gzip等压缩文件时,ADD会帮助我们自动解压缩文件

FROM python:3.9.13-alpine3.14
ADD hello.tar.gz /app/
CMD ["python", "/app/hello.py"]
  1. 如何选择
    因此在COPY和ADD指令中选择的时候,可以遵循这样的原则,所有文件复制均使用COPY指令,仅在需要自动解压的场合使用ADD
  2. 也可以将一个目录打包复制到镜像里面去
FROM python:3.9.13-alpine3.14
ADD app.tar.gz /
CMD ["python3", "/app/hello.py"]
  1. 目录变更的语法命令:WORKDIR
    WORKDIR比cd的好处是,如果没有这个目录,WORKDIR会帮助我们自动创建这个目录
FROM python:3.9.13-alpine3.14
WORKDIR /app/
ADD app.tar.gz ./
CMD ["python3", "./app/hello.py"]

通过Dockerfile构建机镜像
sudo docker image build -t python_test:7.0 . 通过构建好的镜像创建并启动容器测试
sudo docker container run --rm image_id

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

  1. ARG和ENV是经常容易被混淆的两个Dockerfile的语法,都可以用来设置一个变量,但实际上两者有很多的不同
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

通过dockerfile创建镜像
sudo docker image build -f Dockerfile-arg -t ipinfo-arg . 通过构建的镜像创建容器并进入容器
sudo docker container -it image_id 查看ipinfo的版本
ipinfo version

Dockerfile-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

操作步骤同Dockerfile-arg

2. ARG和ENV的区别?

dockerfile 基础镜像 dockerfile基础镜像选型_Dockerfile_03

  • ARG只用于构建镜像的时候使用,构建完成之后,创建容器的时候,容器里面是无法使用ARG的变量的
  • ENV不仅可用于构建镜像的时候使用,创建容器的时候也可以使用ENV变量,ENV变量会永久的保存在镜像里面
  1. ARG可以在构建镜像的时候动态修改value,通过--build-value
    sudo docker image build -f Dockerfile-arg -t ipinfo-arg-new --build-arg VERSION=2.0.0 .
  2. 何时使用ARG和ENV
    当我们仅仅使用变量取构建镜像的时候,我们就使用ARG,当我们不仅仅是用变量去构建镜像,还需在容器中去使用该变量时,就需要使用ENV了

容器启动命令 CMD

  1. CMD可以用来设置容器启动时默认会执行的命令
  • 容器启动时默认执行的命令
  • 如果docker container run启动时指定了其它命令,则CMD命令会被忽略
  • 如果定义了多个CMD,只有最后一个会被执行
    第二个如果运行容器的时候指定了命令,CMD命令就会被忽略,啥意思呢,我们指定个命令看一下:
    sudo docker container run -it ipinfo-env ipinfo version 此时创建的容器只会显示一下ipinfo的版本,并不会进入容器了。
  1. 我们重新定义Dockerfile, 自定义CMD覆盖ubuntu:21.04原始镜像中自带的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
CMD ["ipinfo", "v"]

通过Dockerfile构建镜像
sudo docker image build -t ipinfo-cmd . 通过镜像构建容器
sudo docker container run --rm -it image_id 此时不会进入容器,只会打印ipinfo的版本,因为我们自定的CMD覆盖了原有的

容器启动命令 ENTRYPOINT

  1. ENTRYPOINT也可以设置容器启动时要执行的命令,但是和CMD是有区别的
    CMD设置的命令,可以在docker container run启动时传入其它命令,覆盖掉CMD命令,
    但是ENTRYPOINT所设置的命令是一定会被执行的,
    ENTRYPOINT和CMD可以联合使用,ENTRYPOINT设置执行的命令,CMD传递参数
    Dockerfile-cmd文件:
FROM ubuntu:21.04
CMD ["echo", "hello docker"]

Dockerfile-entrypoint文件:

FROM ubuntu:21.04
ENTRYPOINT ["echo", "hello docker"]

用这两个Dockerfile分别构建镜像,运行容器时额外指定命令看效果:

dockerfile 基础镜像 dockerfile基础镜像选型_linux_04


可以看出,运行容器时如果不指定命令,两者没啥区别,如果指定了命令,CMD命令会被覆盖,而ENTRYPOINT命令不会被覆盖。

  • ENTRYPOINT可以和CMD联合使用,如下
FROM ubuntu:21.04
ENTRYPOINT ["echo"]
CMD ["hello", "world"]

创建镜像
sudo docker image build -f Dockerfile-entrypoint-cmd -t ubuntu-entrypoint-cmd . 运行容器,通过ENTRYPOINT执行命令,通过CMD指定参数:
sudo docker container run --rm -it image_id [hello docker] 注意:如果不传入CMD参数,那么会打印hello world, 如果传入了CMD参数,那么会把默认的CMD命令覆盖掉,打印hello docker

shell格式和exec格式

  1. CMD和ENTRYPOINT同时支持shell格式和exec格式
  • shell格式
    CMD echo "hello world"ENTRYPOINT echo "hello world"
  • Exec格式
CMD ["echo", "hello world"]
ENTRYPOINT ["echo" "hello world"]
  1. 注意shell脚本的问题
FROM ubuntu:21.04
ENV NAME=docker
CMD echo "hello $NAME"

上面这些是没问题的,加入我们把CMD改成Exec格式,是不行的,结果打印:hello $NAME
那么Exec格式需要怎么写才能使用ENV设置的环境变量呢,我们需要以shell脚本的方式去执行

FROM ubuntu:21.04
ENV NAME=docker
CMD ["/bin/sh", "-c", "echo hello $NAME"]

或这样

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

创建镜像,运行容器,执行成功。

Dockerfile中的RUN命令升级版

由于之前RUN命令后面可能会跟有很多的命令,几十行甚至上百行,所以我们把这些命令写到一个脚本文件中,
我们直接 RUN bash xxx.sh文件即可。例如:
Dockerfile文件:

FROM ubuntu:21.04
WORKDIR /usr/src/app/
COPY run.sh run.sh
RUN bash run.sh
CMD ["ipinfo", "v"]

run.sh文件:

#!/usr/bin/env bash

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 -xzvf 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

创建镜像
sudo docker image build -t image_name . 运行容器
sudo docker run --rm -it image_id

一起构建一个python flask镜像

  • 目录结构
  1. Dockerfile文件:
FROM python:3.9-slim

WORKDIR /usr/src/app

COPY app /usr/src/app

RUN bash run.sh

ENTRYPOINT ["flask", "run"]
CMD ["-h", "0.0.0.0", "-p", "5000"]

注意:这里使用了ENTRYPOINT+CMD是因为,我们可以自己指定ip+端口来覆盖CMD命令,ENTRYPOINT是覆盖不了的。
2. run.sh文件

#!/usr/bin/env bash
pip install --no-cache-dir -U -r requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple
  1. app.py文件
from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello():
    return "hello python"

3.1 requirements.txt文件

flask
  1. 通过Dockerfile构建镜像
    sudo docker image build -t flask-demo .
  2. 通过镜像创建并启动容器
    sudo docker container run -it -p 8008:5000 flask-demo

Dockerfile使用技巧,合理使用缓存

在文件没有任何改动的同时去重新构建镜像的话,就会完全使用缓存

sudo docker image build -t flask-demo .

结果:

dockerfile 基础镜像 dockerfile基础镜像选型_Dockerfile_05


我们的Dockerfile是这样的:

FROM python:3.9-slim

WORKDIR /usr/src/app

COPY app /usr/src/app

RUN bash run.sh

ENTRYPOINT ["flask", "run"]
CMD ["-h", "0.0.0.0", "-p", "5000"]

当app目录中有任何一点改动的COPY层以及后面所有的层都会重新构建,浪费时间,
为了解决这个问题,我们可以把经常需要改动的放在镜像层的后面,当然我们要注意移动的层后面的没有依赖前面的层,这样移动才能正常。

总结:我们要合理使用Dockerfile的缓存,我们要把经常要改变的文件目录往Dockerfile后面放。

Dockerfile技巧-dockerignore

dockerfile 基础镜像 dockerfile基础镜像选型_docker_06


我们的代码仅仅有几kb,但是Dockerfile同级目录有其它文件目录较大,所以构建会特别慢,我们需要把这样的目录忽略掉,

那么就需要使用到.dockerignore文件,在Dockerfile同级目录下创建.dockerignore文件

.vscode/
env/
test/

这样构建镜像时就会把这三个目录都忽略掉。
两个作用:一个是减小我们镜像的体积,另一个是保护我们的私密文件

Dockerfile技巧-多阶段构建-C语言

  1. Dockerfile文件:
FROM gcc:9.4 AS builder

COPY hello.c /usr/src/app/hello.c

WORKDIR /usr/src/app

RUN gcc --static -o hello hello.c

FROM alpine:3.13.5

COPY --from=builder /usr/src/app/hello /usr/src/app/hello

ENTRYPOINT ["/usr/src/app/hello"]

CMD []
  1. hello.c文件
#include <stdio.h>

void main(int argc, char *argv[])
{
    printf("hello %s\n", argv[argc - 1]);
}

这样构建镜像分为两个阶段的目的是:第一个阶段需要gcc环境编译c代码为可执行文件,
第二个阶段是因为gcc镜像太大了,而我们运行编译好的文件又不需要gcc环境,仅需要一个小点的linux环境即可,
使得镜像变得非常的小,同时也不影响编译好的代码的执行。

Dockerfile技巧-多阶段构建-golang

  1. 多阶段构建前:
    Dockerfile文件
FROM golang:1.18

COPY main.go /usr/src/app/

WORKDIR /usr/src/app

RUN go build -o httpserver main.go

ENTRYPOINT ["/usr/src/app/httpserver"]

main.go文件:

package main

import (
        "log"
        "net/http"
)

func main() {
        mux := http.NewServeMux()
        mux.HandleFunc("/index", func(writer http.ResponseWriter, request *http.Request) {
                _, _ = writer.Write([]byte("hello index"))
        })

        err := http.ListenAndServe("0.0.0.0:8008", mux)
        if err != nil {
                log.Fatalln(err.Error())
        }
}

构建镜像:sudo docker image build -t go-demo . 构建好的镜像大小:占了971MB,
REPOSITORY TAG IMAGE ID CREATED SIZE
go-demo latest 85a4e8f7dcc9 4 minutes ago 971MB
2. 多阶段构建后,main.go文件不变
Dockerfile文件:

FROM golang:1.18 AS builder

COPY . /usr/src/app/

WORKDIR /usr/src/app

RUN bash run.sh

FROM alpine:3.16.0

COPY --from=builder /usr/src/app/httpserver /usr/src/app/httpserver

ENTRYPOINT ["/usr/src/app/httpserver"]

run.sh文件

#!/usr/bin/env bash

go env -w CGO_ENABLED=0
go build -o httpserver main.go

构建镜像:sudo docker image build -t go-alpine . 启动容器:sudo docker container run --rm -it -p 8008:8008 go-alpine 查看镜像大小,可以看到镜像10几兆就搞定了,非常节约空间。

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

参考文档