版本比较
标识
- 该行被添加。
- 该行被删除。
- 格式已经改变。
基础镜像的选择
基本原则
- 官方镜像优于非官方的镜像,如果没有官方镜像,则尽量选择Dockerfile开源的。
- 固定版本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 |
提示 |
---|
alpine是一个体积非常小的Linux发行版,大小只有5MB,除了提供Linux必要的运行环境,基本不附带其他工具。 大部分受欢迎的镜像都会提供基于alpine制作的版本,比如python3.9.5-alpine。 |
示例:Build一个Nginx镜像
代码块 | ||
---|---|---|
| ||
<h1>Hello Docker</h1> |
代码块 | ||
---|---|---|
| ||
FROM nginx:1.21.0-alpine ADD index.html /usr/share/nginx/html/index.html |
构建镜像:
代码块 |
---|
docker image build -t mynginx-alpine . |
延申阅读
- https://pythonspeed.com/articles/base-image-python-docker-images/
- https://pythonspeed.com/articles/alpine-docker-python/
通过 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:
代码块 |
---|
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> 4 minutes ago RUN /bin/sh -c mv ipinfo_2.0.1_linux_amd64 /… 9.36MB buildkit.dockerfile.v0 <missing> 4 minutes ago RUN /bin/sh -c tar zxf ipinfo_2.0.1_linux_am… 9.36MB buildkit.dockerfile.v0 <missing> 4 minutes ago RUN /bin/sh -c wget https://github.com/ipinf… 4.85MB buildkit.dockerfile.v0 <missing> 4 minutes ago RUN /bin/sh -c apt-get install -y wget # bui… 7.58MB buildkit.dockerfile.v0 <missing> 4 minutes ago RUN /bin/sh -c apt-get update # buildkit 33MB buildkit.dockerfile.v0 <missing> 4 days ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B <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> 4 days ago /bin/sh -c #(nop) ADD file:d6b6ba642344138dc… 74.1MB |
改进版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> 4 days ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B <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> 4 days ago /bin/sh -c #(nop) ADD file:d6b6ba642344138dc… 74.1MB $ |
提示 |
---|
尽量将RUN指令都写到一行里。 |
文件复制和目录操作
往镜像里复制文件有两种方式,COPY
和 ADD
, 我们来看一下两者的不同。
复制普通文件
COPY
和ADD
都可以把一个本地的文件复制到镜像里,如果目标目录不存在,则会自动创建。比如把本地的 hello.py 复制到 /app 目录下,如果 /app这个路径不存在,则会自动创建。
代码块 |
---|
FROM python:3.9.5-alpine3.13 COPY hello.py /app/hello.py |
复制压缩文件
ADD
比COPY
高级一点的地方就是,如果复制的是一个gzip等压缩文件时,ADD会帮助我们自动去解压缩文件。
代码块 |
---|
FROM python:3.9.5-alpine3.13 ADD hello.tar.gz /app/ |
提示 |
---|
在 |
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:
代码块 |
---|
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:
代码块 |
---|
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 |
区别:
提示 | ||
---|---|---|
除此之外,ARG声明的变量可以在构建的时候动态修改,使用
|
容器启动命令 CMD
CMD可以用来设置容器启动时默认会执行的命令。
代码块 |
---|
CMD ["可执行参数", "参数1", "参数2", ...] |
- 容器启动时默认执行的命令。容器启动时默认执行的命令
- 如果docker container run启动容器时指定了其它命令,则CMD命令会被忽略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。
代码块 |
---|
$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> 24 hours ago ENV VERSION=2.0.1 0B buildkit.dockerfile.v0 |
<missing> 7 days ago /bin/sh -c #(nop) CMD ["/bin/bash"] 0B |
<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> 7 days ago /bin/sh -c #(nop) ADD file:d6b6ba642344138dc… 74.1MB |
容器启动命令 ENTRYPOINT
ENTRYPOINT 也可以设置容器启动时要执行的命令,但是和CMD是有区别的。
CMD
设置的命令,可以在docker container run 时传入其它命令,覆盖掉CMD
的命令,但是ENTRYPOINT
所设置的命令是一定会被执行的。ENTRYPOINT
和CMD
可以联合使用,ENTRYPOINT
设置执行的命令,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 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所定义的命令则无法覆盖,一定会执行
$ 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格式
CMD echo "hello docker"
ENTRYPOINT echo "hello docker"
Exec格式
以可执行命令的方式
ENTRYPOINT ["echo", "hello docker"]
CMD ["echo", "hello docker"]
注意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脚本的方式去执行
FROM ubuntu:21.04 ENV NAME=docker CMD ["sh", "-c", "echo hello $NAME"]
一起构建一个 Python Flask 镜像
Python 程序
from flask import Flask app = Flask(__name__) @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"]
Dockerfile 技巧——合理使用缓存
Dockerfile 技巧——合理使用 .dockerignore
什么是Docker build context
Docker是client-server架构,理论上Client和Server可以不在一台机器上。
在构建docker镜像的时候,需要把所需要的文件由CLI(client)发给Server,这些文件实际上就是build 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)
$ 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所指向的目录
.dockerignore 文件
.vscode/ env/
有了.dockerignore文件后,我们再build, build context就小了很多,4.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语言例子
假如有一个C的程序,我们想用Docker去做编译,然后执行可执行文件。
#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
FROM gcc:9.4 COPY hello.c /src/hello.c WORKDIR /src RUN gcc --static -o hello hello.c ENTRYPOINT [ "/src/hello" ] CMD []
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
实际上当我们把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
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权限
[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 镜像` :
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
目录 |
---|