Docker指令详解&最佳实践&面试问题
docker已成为现在流行的一个开源容器引擎,我们在工作中也经常要与其打交道。这篇文章将主要从以下四个大方面阐述作者对docker的理解,希望能帮助大家由浅入深的或者是查漏补缺的更好的使用docker,欢迎大家在评论区留言交流。
- docker里的一些基本概念
- docker常用指令详解
- Dockerfile最佳实践
- docker面试题
docker里的一些基本概念
-
镜像和容器的关系
由图中可以看出,红色部分的只读层为镜像,而容器就是多层可读层+一层读写层。
-
image镜像
镜像是由一堆可读层组成,除了最下边一层,每层都有一个指向下层的指针。image由Dockerfile构建而来,每层都对应一个dockerfile指令,例如下边的Dockerfile,每个命令会创建一层镜像。
FROM ubuntu:18.04 COPY . /app RUN make /app CMD python /app/app.py
-
container容器
容器=镜像+读写层。可以通过docker run [image]从一个镜像运行一个容器。
-
Dockerfile
指定镜像的生成规则,可以通过docker build -f [Dockerfile]生成一个镜像。
-
容器是怎么来的
docker指令
根据上一部分我们知道了一个容器是怎么来的,下边列出的是作者在平时工作中经常会使用到的一些指令以及详解。
-
Dockerfile
假如我们有如下Dockerfile,关于Dockerfile的详解会在下一部分。# syntax=docker/dockerfile:1 FROM node:12-alpine copy hello.js ./hello.js #hello.js内容为console.log('hello world'); CMD ["node", "hello.js"]
-
build
我们可以通过build指令从Dockerfile生成一个image。
-t
表示我想要生成image的name和一个可选的tag,格式为name:tag。.
注意:这里的.
并不是表示Dockerfile的位置,而是此次构建的context,后边会详解。那么大家可能注意到我们并没有指定Dockerfile的路径,因为build默认会从当前目录寻找一个叫
Dockerfile
的文件。如果你的Dockerfile不在当前路径呢,我们可以使用-f [path]
来指定其路径。
docker build -t hello:v1 .
其他常用的和image相关的指令还有:
- docker images:查看当前宿主机上所有的image
- docker tag hello:v1 hello:v2:把image的tag从v1变成v2
- docker rmi [image-id]:删除一个指定的image
-
run
docker run hello:v1
我们可以把容器运行起来了。当然docker run有很多参数,我经常用的有:
-d
: 以detached mode运行-p
: 后加3000:3000即表示把容器内的3000端口发布到宿主机的3000端口上-v
: /src:/src,把宿主机的src目录绑定到容器内-it
: 以交互模式运行container
其他常用的和container相关的指令还有:
- docker ps -a:查看所有宿主机上的container以及其状态等信息
- docker stop [container-id]:停止一个容器
- docker rm [container-id]:删除一个容器,-f表示强制。
Dockerfile常用指令详解
下图列出的是笔者记录的需要常用的一些指令及其特点。
其中包括的要点有:
-
ADD和COPY的区别,分别在什么时候使用:
- 所有的文件复制都使用copy命令
- add 的src可以是一个远程url,如果是可解压文件如 tar.gz, 会自动解压
-
数据的两种绑定方式,使用
docker inspect [container-id]
可以找到Mounts字段查看- 目录绑定,type=bind
- 数据卷绑定,type=volume
-
dockerfile里设置的VOLUME字段的作用:
FROM ubuntu VOLUME ["/data1","/data2"]
注意:dockerfile里的volume只能指定容器内的目录,不能指定主机的目录,可以在docker run的时候通过
-v
进行覆盖,如果不覆盖,会默认在/var/lib/docker/volumes/xxx创建匿名volume。所以dockerfile里的VOLUME字段的作用是为了防止用户忘记在运行容器时忘记将动态文件挂在为卷。 -
CMD和ENTRYPOINT的区别:
- CMD可以作为ENTRYPOINT的参数,docker把CMD命令拼接到ENTRYPOINT之后
- CMD可以在run时候被参数覆盖,而ENTRYPOINT不可以
- ENTRYPOINT可以执行一个脚本,这样CMD的参数会被脚本接收
- 一般最佳实践是将ENTRYPOINT设置为容器的main command
- 多个CMD只有最后一个生效,同理ENTRYPOINT也一样。
-
CMD和ENTRYPOINT的两种语法:
- EXEC写法:CMD [“executable”,“param1”,“param2”]
- SHELL写法:CMD command param1 param2
- 永远使用EXEC写法
-
ENV和RUN export的区别:
-
通过ENV设置的会持续存在,包括multiStage的dockerfile
-
RUN export只会在当前image层有效,如下例子:
FROM centos:6 ENV FOO=foofoo RUN export BAR=barbar RUN export BAZ=bazbaz && echo "$FOO $BAR $BAZ"
build这个image,最后一步的输出结果为:
Step 4/4 : RUN export BAZ=bazbaz && echo "$FOO $BAR $BAZ" ---> Running in eb66196b238d foofoo bazbaz
我们可以看到,
FOO
是输出来了,因为ENV可以跨多层生效,BAR
没有输出,而BAZ
输出了,是因为RUN export只存在于当前层。
-
-
ARG命令
-
ARG指定的参数不能在容器运行时读取
-
ARG指定的参数可以在docker build时候通过—build-arg覆盖
-
对于multiStage的dockerfile,需要在FROM之后使用如下指令重新指定一下,这是因为ARG只在一当前它所指定的stage中生效
ARG DARSHAN_VER=3.1.6 FROM fedora:29 as build ARG DARSHAN_VER RUN curl -O "ftp://ftp.mcs.anl.gov/pub/darshan/releases/darshan-${DARSHAN_VER}.tar.gz" \ && tar .. FROM fedora:29 ARG DARSHAN_VER COPY --from=build "/usr/local/darshan-${DARSHAN_VER}" "/usr/local/darshan-${DARSHAN_VER}" ..
-
WORKDIR
WORKDIR a WORKDIR b WORKDIR c RUN pwd #a/b/c
-
multiStage多阶段构建
docker从17.5开始支持多阶段构建,是为了解决:
- 镜像层次多,体积过大,部署时间长
- 源代码可能泄露
通过多个FROM指定多个构建阶段,前边阶段可能是多个builder,把最终需要delivery的binary或其他file 复制到最终镜像。
FROM golang:alpine as builder RUN apk --no-cache add git WORKDIR /go/src/github.com/go/helloworld/ RUN go get -d -v github.com/go-sql-driver/mysql COPY app.go . RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app . FROM alpine:latest as pr RUN apk --no-cache add ca-certificates WORKDIR /root/ COPY --from=0 /go/src/github.com/go/helloworld/app . CMD ["./app"]
可以通过如下指令build某个stage的image:
docker build --target builder -t alexellis2/href-counter:latest .
Dockerfile最佳实践
-
keep small
-
使用合适的base image,例如openJDK代替Ubuntu + jdk;常用的linux镜像一般有Ubuntu,CentOS,Alpine,一般更推荐使用Alpine,其大小分别如下:
-
如果你要运行的包包含所有的依赖,那么可以使用空的image:
scratch
... # 运行:使用scratch作为基础镜像 FROM scratch as prod # 在build阶段复制可执行的go二进制文件app COPY --from=builder /go/release/app / # 启动服务 CMD ["/app"]
-
多阶段构建,使用maven build java app,然后tomcat deploy
-
-
高效利用缓存,尽量把变化最小的放在前边。例如把需要下载的依赖和业务代码分开:
-
对于前端项目,可以
ADD package.json ./ RUN npm i ADD src ./ RUN npm run build # 这样如果package.json不变,可以缓存前两步,不会重新执行npm i
-
对于golang项目,可以
ADD go.mod go.sum ./ RUN go mod download ADD server ./ RUN go build app.go
-
-
利用.dockerignore来减小build context,可以提高docker load context的速度,例如:
# .dockerignore node_modules/ .git/
-
指令串联,一方面可以减少image分层,缩小体积;另一方面避免达到最大层数(127)。
例如多个RUN合并为一个:
FROM debian:stretch RUN set -x; buildDeps='gcc libc6-dev make wget' \ && apt-get update \ && apt-get install -y $buildDeps \ && wget -O redis.tar.gz "http://download.redis.io/releases/redis-5.0.3.tar.gz" \ && mkdir -p /usr/src/redis \ && tar -xzf redis.tar.gz -C /usr/src/redis --strip-components=1 \ && make -C /usr/src/redis \ && make -C /usr/src/redis install \ && rm -rf /var/lib/apt/lists/* \ && rm redis.tar.gz \ && rm -r /usr/src/redis \ && apt-get purge -y --auto-remove $buildDeps
-
执行yum install -y 的时候可以一次install多个工具,例如:
yum install -y gcc gcc-c++ make
-
执行apt-get install -y的时候增加选项— no-install-recommends,去除非必须的依赖
-
使用RUN echo $(ls -al) 代替 RUN ls
在实际使用的过程中发现ls不会输出内容,加上echo后可以了。不知道大家有没有遇到这种情况。
-
优先使用copy over add。
Docker面试题
-
Docker和虚拟机的区别
传统虚拟机是虚拟化出来一套硬件,在其上运行操作系统,而docker是直接运行在宿主的内核,没有自己的内核,也没有硬件虚拟化,更加轻便。
-
ADD和COPY的区别,见上边。
-
docker-compose vs k8s vs docker swarm
- docker-compose: 单机node集群编排
- k8s:多主机集群
- swarm:多主机集群
-
还有其他一些,基本上文都覆盖到了,也欢迎大家补充。
总结
本文主要介绍了笔者在工程中的一些常见docker用法,可能需要有一些docker基础之后食用更佳,欢迎评论区留言讨论。欢迎关注公众号二蛋实验室
, 聊一聊golang,react,前后端优化以及股票投资等,共同进步,握手。