第四章-Dockerfile完全指南
- 如何选择基础镜像
基本原则:
- 官方镜像优于非官方镜像,如果没有官方镜像,则尽量选择Dockerfile开源的;
- 固定版本tag,而不是每次都使用最新版本latest
- 尽量选择体积小的镜像
- 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执行指令
- 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 .
查看镜像和镜像对应的分层
可以看到,每个RUN命令,都给镜像增加了一层,使得镜像非常的臃肿,所以我们需要优化生成镜像的命令,
- 将多个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也变小了
文件的复制和目录操作
- 往镜像里复制文件有两种方式,ADD和COPY,我们来看一下两者的不同
COPY和ADD都可以把local的一个文件复制到镜像里,如果目标目录不存在,则会自动创建 - 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"]
- 如何选择
因此在COPY和ADD指令中选择的时候,可以遵循这样的原则,所有文件复制均使用COPY指令,仅在需要自动解压的场合使用ADD - 也可以将一个目录打包复制到镜像里面去
FROM python:3.9.13-alpine3.14
ADD app.tar.gz /
CMD ["python3", "/app/hello.py"]
- 目录变更的语法命令: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)
- 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的区别?
- ARG只用于构建镜像的时候使用,构建完成之后,创建容器的时候,容器里面是无法使用ARG的变量的
- ENV不仅可用于构建镜像的时候使用,创建容器的时候也可以使用ENV变量,ENV变量会永久的保存在镜像里面
- ARG可以在构建镜像的时候动态修改value,通过--build-value
sudo docker image build -f Dockerfile-arg -t ipinfo-arg-new --build-arg VERSION=2.0.0 .
- 何时使用ARG和ENV
当我们仅仅使用变量取构建镜像的时候,我们就使用ARG,当我们不仅仅是用变量去构建镜像,还需在容器中去使用该变量时,就需要使用ENV了
容器启动命令 CMD
- CMD可以用来设置容器启动时默认会执行的命令
- 容器启动时默认执行的命令
- 如果docker container run启动时指定了其它命令,则CMD命令会被忽略
- 如果定义了多个CMD,只有最后一个会被执行
第二个如果运行容器的时候指定了命令,CMD命令就会被忽略,啥意思呢,我们指定个命令看一下:sudo docker container run -it ipinfo-env ipinfo version
此时创建的容器只会显示一下ipinfo的版本,并不会进入容器了。
- 我们重新定义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
- 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分别构建镜像,运行容器时额外指定命令看效果:
可以看出,运行容器时如果不指定命令,两者没啥区别,如果指定了命令,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格式
- CMD和ENTRYPOINT同时支持shell格式和exec格式
- shell格式
CMD echo "hello world"
ENTRYPOINT echo "hello world"
- Exec格式
CMD ["echo", "hello world"]
ENTRYPOINT ["echo" "hello world"]
- 注意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镜像
- 目录结构
- 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
- app.py文件
from flask import Flask
app = Flask(__name__)
@app.route("/")
def hello():
return "hello python"
3.1 requirements.txt文件
flask
- 通过Dockerfile构建镜像
sudo docker image build -t flask-demo .
- 通过镜像创建并启动容器
sudo docker container run -it -p 8008:5000 flask-demo
Dockerfile使用技巧,合理使用缓存
在文件没有任何改动的同时去重新构建镜像的话,就会完全使用缓存
sudo docker image build -t flask-demo .
结果:
我们的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
我们的代码仅仅有几kb,但是Dockerfile同级目录有其它文件目录较大,所以构建会特别慢,我们需要把这样的目录忽略掉,
那么就需要使用到.dockerignore文件,在Dockerfile同级目录下创建.dockerignore文件
.vscode/
env/
test/
这样构建镜像时就会把这三个目录都忽略掉。
两个作用:一个是减小我们镜像的体积,另一个是保护我们的私密文件
Dockerfile技巧-多阶段构建-C语言
- 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 []
- 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
- 多阶段构建前:
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用户