Docker(一) Brief Summary (Dockerfile、多阶段构建、镜像代理)

1. Docker介绍

1.1. Docker概述

Docker是一个DevOps方面的基础工具和平台,通过轻量级的容器将应用程序和基础架构分离,以便应用快速部署、扩容、和交付软件。

Docker支持不同系统部署运行,Docker提供了基于容器来打包和运行程序,通过Doker可以做到多环境的代码一致

Docker提供了工具和平台来管理容器的生命周期:

  1. 基于容器,本地研发应用程序
  2. 容器成为测试和分发部署的单元
  3. 测试验证通过后,将应用通过容器部署到生产环境,让通过调度和协调容器,做到应用程序的自动伸缩

1.1.1. Docker Engine

  1. Docker引擎基于CS模式开发,组件包含:

    • Client: 客户端就是以docker命令行界面
    • Server:Docker Daemon负责创建和管理Docker对象(如镜像、容器、网络、卷),基于REST API向docker命令行、GUI、其他Docker应用提供操作服务
  2. 相关组件:

1.1.2. Docker可以拿来干些什么?

  1. 标准且一致的应用程序交互流程:通过Docker镜像可以实现标准化,适合CI/CD流程,到达程序与环境在研发、测试、预发布、生产一致
  2. 动态扩缩容,资源的高效使用:镜像的可移植、容器的轻量级,方便实现更轻松和动态管理工作负载,按业务及时扩展或拆除应用和服务
  3. 相比虚拟机,可以运行更多的工作负载,占用资源更小

1.1.3. Docker整体架构

  • Client:客户端通过RestAPI与Docker主机交互
    • 比如执行docker run,客户端会将命令发送给到远程Docker主机的dockerd服务,然后执行该命令,运行一个容器
    • 客户端可以与多个Docker主机通信
  • Docker主机:
    • 运行有dockerd的Docker Server Daemon,负责镜像、容器、网络、卷的管控,同时还可以与其他Docker服务通信,一起组成Swarm模式,提供服务
  • Registry镜像注册表:
    • 负责镜像的仓库管控功能,DockerHub是公共的注册中心
    • 命令行执行docker pulldocker push时候,会通知dockerd服务拉取或推送镜像到镜像注册表中

1.1.4. Docker 对象

上面在dockerd服务中有提到其功能是负责Docker对象的操作,Docker对象包含有镜像、容器、网络、卷、插件和其他对象

  • Image镜像:
    • 只读用于创建一个Docker容器的指令模板(称为Dockerfile),通常是基于另一个镜像,并带有一些额外的自定义配置(比如基于phpfpm并外加一些应用程序的配置详细信息)
    • Dockerfile中的每条指令都在图像中创建一个图层,更改Dockerfile并重建映像时,仅重建已更改的那些层。与其他虚拟化技术相比,这是使图像如此轻量,小巧和快速的部分原因(最佳实践中有提及构建的docker镜像层数不要过多,通过命令的拼接可以做到)
    • 镜像可以通过拉取或推送到镜像仓库中
  • Container容器:
    • 容器是镜像的实例,可以通过命令行创建、查看、更改、暂停、停止、删除容器,可以基于容器创建新的镜像(尽量不要)
    • 可以将容器连到一个或多个网络,将存储卷与之配置,默认情况下容器与其他容器或主机是相对隔离,可以控制容器的网络、存储或其他基础系统与其他容器的隔离程度
    • 容器是基于镜像配置创建或启动的,删除容器后,容器中任何未存储在持久存储中的更改都会消失
  • Service服务:
    • 服务支持跨多个docker主机或dockerd服务扩展容器,运行将这些容器连接在一起工作(基于Swarm模式,通过swarm init初始化或者swarm join加入到一个Swarm集群中)
    • Swarm模式的每个成员都是Dockerd服务(称之为一个节点),Dockerd服务通过RestAPI进行交互
    • 默认情况下,服务在所有工作节点之间进行负载均衡,对服务消费者来说,Dockerd服务就等同于一个单独的应用程序

1.1.5. Docker容器运行过程

docker命令行:$ docker run -i -t ubuntu /bin/bash

  1. docker发送该命令到dockerd服务
  2. dockerd服务寻找本地的ubuntu:latest镜像,如果本地没有则会从镜像仓库拉取到dockerd主机,类似于:docker pull
  3. 镜像拉取后,会开始创建一个容器,类似于:docker container create
    • 创建一个随机卷,将读写文件系统分配给容器,作为其最后一层(由于没有指定容器名称,dockerd服务会随机指派一个容器名称)
    • 创建一个网络接口,将容器连接到默认网络(这包括容器IP地址分配,默认情况下,容器可以通过Docker主机网络连接到外部)
  4. dockerd服务启动容器,并执行/bin/bash,通过-i(–interactive,打开标准输入)和-t(–tty),容器以交互方式运行并连接到终端,可以使用键盘提供输入,同时将输出记录到终端。
  5. 键入exit以终止/bin/bash命令时,容器会停止但不会被删除

1.1.6. Docker的底层技术

Docker基于Go开发,利用了Linux内核的几个功能来提供Docker的功能:

  • Namespaces:
    • 基于namespace来提供隔离工作空间的技术(称之为容器),运行容器,会创建一组命名空间
    • PID命名空间:进程隔离
    • NET命名空间:管理管理
    • IPC命名空间:IPC进程通信管控
    • MNT命名空间:文件系统挂载点管控
    • UTS命名空间:内核版隔离
  • Control groups(cgroups)
    • 限制进程的资源集(比如硬件资源共享和限制)
  • Union file systems
    • Docker使用UnionFS来为容器构建文件系统
    • Docker还支持其他UnionFS变体(AUFS\VFS等)
  • Container format
    • Docker引擎将命名空间、cgroups、UnionFS组合一起成容器格式,默认的容器格式是libcontainer(还有其他容器格式支持)

1.2. 代理加速

1.2.1. Register仓库镜像代理加速

"azure": "http://dockerhub.azk8s.cn",
"tencent": "https://mirror.ccs.tencentyun.com",
"netease": "http://hub-mirror.c.163.com",
"ustc": "https://docker.mirrors.ustc.edu.cn",
"aliyun": "https://2h3po24q.mirror.aliyuncs.com"
  • Aliyun针对用户的特定官方镜像加速说明:https://help.aliyun.com/document_detail/60750.html
  • Aliyun官方加速直通车(需阿里云账号):https://cr.console.aliyun.com/cn-hongkong/instances/mirrors

1.2.2. Alpine仓库代理加速

alpine仓库在docker构建过程中也很慢,可以改成阿里云镜像(另外如果是socks代理,wget是无法解析的)

// 阿里云镜像
sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories
// 科大镜像
sed -i 's/dl-cdn.alpinelinux.org/mirrors.ustc.edu.cn/g' /etc/apk/repositories

1.2.3. Golang模块代理

参考:https://goproxy.io/,配置了代理后,安装速度还是杠杠的!

ENV GOPROXY=https://goproxy.io/

1.3. Mac使用Docker的相关FAQ

基于Docker Desktop for Mac

1.3.1. Docker服务证书

参考:https://docs.docker.com/docker-for-mac/#add-custom-ca-certificates-server-side

// 可以向Docker守护程序添加受信任的证书颁发机构(CA),用于验证注册表服务器证书
~/.docker/certs.d

/etc/docker/certs.d/        <-- Certificate directory
└── localhost:5000          <-- Hostname:port
   ├── client.cert          <-- Client certificate
   ├── client.key           <-- Client key
   └── ca.crt               <-- Certificate authority that signed
                                the registry certificate

1.3.2. Docker命令自动提示

参考:https://docs.docker.com/docker-for-mac/#install-shell-completion

etc=/Applications/Docker.app/Contents/Resources/etc
ln -s $etc/docker.bash-completion $(brew --prefix)/etc/bash_completion.d/docker
ln -s $etc/docker-machine.bash-completion $(brew --prefix)/etc/bash_completion.d/docker-machine
ln -s $etc/docker-compose.bash-completion $(brew --prefix)/etc/bash_completion.d/docker-compose

1.3.3. 网络限制

  • 网络特性:VPN、Docker主机容器端口映射、HTTP/HTTPS代理
  • 网络限制:
    1. macOS上没有docker0网桥
    2. 无法ping通容器IP
    3. 无法从macOS主机访问docker(Linux)桥接网络
  • Mac下的Docker网络限制变通:
    1. 从容器连接到MAC主机上的服务:基于主机IP或者基于特殊的DNS解析(仅用于开发目的):
      • 主机域名: host.docker.internal
      • 网关域名:gateway.docker.internal
    2. 从MAC连接到一个容器:
      • 端口转发:docker run -P 或者docker run -p port1:port2

1.3.4. 磁盘存储

  • 使用-v bind mount$ docker run -it -v ~/Desktop:/Desktop r-base bash,将主机与容器文件系统绑定
  • 镜像磁盘存储查看:$ docker image ls
  • 移除多余镜像:$ docker system prune

2. Dockerfile相关使用

这里仅做一些概要介绍,细节后续单独梳理

2.1. 应用开发最佳实践

参考:https://docs.docker.com/develop/dev-best-practices/

  1. 保持Image最小:
    • 基于官方适当的基本镜像;
    • 使用多级构建,将构建出来App的镜像,复制到正确的位置(最终图像不包含构建所引入的所有库和依赖项,而只包含运行它们所需的工件和环境);
    • 最小化RUN Dockerfile中单独命令的数量来减少图像中的图层数(shell命令组合);
    • 多个共同点的图像,考虑使用共享组件创建基本图像,并在其上创建独特的图像;
    • 生产映像保持精简但允许调试,请考虑使用生产映像作为调试映像的基本映像;
    • 不要依赖自动创建的latest标签,始终使用有用的标记对其进行标记(比如版本号或者prod、test、dev等)
  2. 应用程序数据生成:
    • 避免存储在容器的可写层中,使用卷或绑定装载
    • 使用卷存储数据
    • 注意加密敏感应用程序数据,对非敏感数使用配置文件
  3. 使用CI/CD进行测试和部署
...
// build过程中,会产生两个图层
RUN apt-get -y update
RUN apt-get install -y python
// build过程中,会产生一个图层
RUN apt-get -y update && apt-get install -y python

2.2. 编写Dockerfiles的最佳实践

参考:https://docs.docker.com/develop/develop-images/dockerfile_best-practices/

Docker通过从一个Dockerfile文本文件中读取指令来自动构建图像

2.2.1. 通用准则和建议

  • 容器足够轻量级,已无状态方式运行容器
  • 基于上下文构建(当面运行docker命令的位置),注意不要包含镜像无需的内容,保证镜像足够小(方便构建、推送和拉取)
  • 就STDIN构建(考虑模板+sed替换),然后通过STDIN,通过docker命令传递给dockerd服务执行
  • 利用.dockerignore忽略不必要的文件
  • 使用多阶段构建(将构建环境与应用运行环境分离)
  • 不安装多余不必要的包
  • 解耦应用程序(应用、Web服务、数据库服务),将应用程序分离到多个容器中可以更容易地水平扩展和重用容器。
  • 最小化镜像层次(RUN、AOPY、ADD会创建图层),并使用多阶段构建,降低图层数量和镜像大小
  • 参数过多,按字母排序和多行分割(利用\),方便阅读和维护
  • Dockerfile默认执行过程中会检测指令是否在缓存中可用(可以基于docker build --no-cache=true忽略缓存)
// 基于stdin构建
echo -e 'FROM busybox\nRUN echo "hello world"' | docker build -
// 等价于
docker build -<<EOF
FROM busybox
RUN echo "hello world"
EOF

2.2.2. 多阶段构建

将不常改动的图层缓存起来,可以大幅度减少最终镜像大小:

  1. 安装构建应用程序所需工具(不常变动)
  2. 安装或更新库依赖(一般,可能不同阶段依赖不同的外部库)
  3. 生成的应用程序(随业务变动较多)

见Golang构建部分示例!

2.2.3. 基本Docker指令

  • FROM
  • LABEL:帮助按项目组织图像,记录许可信息,辅助自动化或其他原因
  • RUN:
    • 避免RUN apk upgrade
    • 始终在同一 声明中结合RUN apk update使用,确保Dockerfile安装最新的软件包版本,无需进一步编码或手动干预
    • 注意善后清除,apt缓存不存储在图层中
    • 支持管道操作
  • CMD
    • 服务类:CMD ["apache2","-DFOREGROUND"]
    • 交互式shell:CMD ["python"]CMD ["php", "-a"]
    • ENTRYPOINT: CMD ["param1"]
  • EXPOSE
    • 指示容器侦听连接的端口
  • ENV
    • 要使新软件更易于运行,可以使用ENV更新容器的环境变量
    • 使用RUN带有shell命令的命令,可以处理ENV变量在容器中也生效
  • ADD/COPY
    • COPY优选,语义明确
    • ADD含带一些功能(比如解压缩),不要直接ADD远程URL(应该使用curl或wget,下载解压安装移除)
    • COPY支持命名的多阶段构建:COPY --from <name> src dst
    • COPY中的src为docker执行命令的上下文,将其发送给docker守护进程,若为mu
  • ENTRYPOINT
    • 最好的用法ENTRYPOINT是设置图像的主命令,允许该图像像该命令一样运行
  • VOLUME
  • USER
    • 可以修改服务的运行用户在非root用户下
  • WORKDIR
    • 应该始终使用绝对路径
  • ONBUILD
    • ONBUILD命令将当前执行后Dockerfile构建完成。 ONBUILD在任何导出FROM当前图像的子图像中执行。将该ONBUILD命令视为父母Dockerfile给孩子的指令Dockerfile。
    • ONBUILD对于将要构建FROM给定图像的图像非常有用。(例如使用ONBUILD一个语言堆栈映像来构建在该语言中编写的任意用户软件的Dockerfile)
    • 构建的图像ONBUILD应该获得单独的标记:ruby:1.9-onbuild
// FROM
FROM golang:1.12-alpine as builder
// LABEL
LABEL com.example.version="0.0.1-beta" \
    com.example.release-date="2019-02-12"
// 更新指令清晰
RUN apk update && apk add --no-cache \
    package-bar \
    package-baz \
    package-foo
EXPOSE 8080

3. Docker构建

3.1. 建造者模式(流程有一定冗余,分步骤进行)

很常见的是有一个Dockerfile用于开发(其中包含构建应用程序所需的所有内容),以及一个用于生产的精简版Dockerfile,它只包含您的应用程序以及运行它所需的内容。这被称为“建造者模式”。维护两个Dockerfiles并不理想,但一些特定情况下还是有用(比如构建环境的单独抽离)

  1. 通过第一个容器build出来go应用;
  2. 将build出来的go应用,copy出来到当前目录;
  3. build第二个容器,将go应用复制进去;

Dockerfile.build

// Dockerfile.build (Golang应用编译)
FROM golang:1.7.3
WORKDIR /go/src/github.com/alexellis/href-counter/
COPY app.go .
RUN go get -d -v golang.org/x/net/html \
  && CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .

Dockerfile(Golang应用构建)

// Dockerfile(Golang应用构建)
FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY app .
CMD ["./app"]

build.sh

// build.sh
// !/bin/sh
echo Building alexellis2/href-counter:build

docker build --build-arg https_proxy=$https_proxy --build-arg http_proxy=$http_proxy \  
    -t alexellis2/href-counter:build . -f Dockerfile.build

docker container create --name extract alexellis2/href-counter:build  
docker container cp extract:/go/src/github.com/alexellis/href-counter/app ./app  
docker container rm -f extract

echo Building alexellis2/href-counter:latest

docker build --no-cache -t alexellis2/href-counter:latest .
rm ./app

3.2. 使用多阶段构建

对于多阶段构建,您可以FROM在Dockerfile中使用多个语句。每条FROM指令可以使用不同的镜像基础,并且每个指令都开始构建的新阶段。

最终结果是与以前相同的微小生产图像,复杂性显着降低。COPY --from=0行仅将前一阶段的构建工件复制到此新阶段。Go SDK和任何中间工件都被遗忘,而不是保存在最终图像中

不需要创建任何中间图像,也不需要将任何工件提取到本地系统。

解决上述Go应用的分两步构建

// Dockerfile 多阶段构建
FROM golang:1.7.3
WORKDIR /go/src/github.com/alexellis/href-counter/
RUN go get -d -v golang.org/x/net/html  
COPY app.go .
RUN CGO_ENABLED=0 GOOS=linux go build -a -installsuffix cgo -o app .
// 基于第一阶段构建结果
FROM alpine:latest  
RUN apk --no-cache add ca-certificates
WORKDIR /root/
COPY --from=0 /go/src/github.com/alexellis/href-counter/app .
CMD ["./app"]  

3.2.1. 指定命名构建阶段

默认情况下,第一个FROM镜像从0标识,可以通过AS <NAME>指示:

FROM golang:1.7.3 AS builder
...
COPY --from=builder /go/src/github.com/alexellis/href-counter/app .

3.2.2. 指定命名构建阶段生成镜像

构建映像时,不一定需要构建整个Dockerfile,可以构建指定的阶段,一些场景有需要:

  1. 调试指定的构建阶段镜像
  2. 使用Debug的调试阶段
  3. 使用Testing的测试阶段
$ docker build --target builder -t alexellis2/href-counter:latest .

3.2.3. COPY还可以基于外部镜像

COPY --from=nginx:latest /etc/nginx/nginx.conf /nginx.conf

3.3. Golang应用构建

Golang和容器一起使用,应用构建这块,有两种方式:

  1. Go支持跨平台交叉编译,可以直接在开发机器上面Build目标机器的执行文件:GOOS=linux go build -o hello hello.go
  2. 或者在Docker构建后,生成Go二进制应用(编译GO应用的环境可以复用)

3.3.1. 构建环境和应用未分离

使用Go容器作为构建和运行时环境,Docker镜像包很大

// Dockerfile [未优化]
// BaseImage setting
FROM golang:1.12-alpine as builder
RUN  apk --no-cache add git curl tcpdump

// Env setting
ENV ServerName serverA
ENV ServerPort 8851

// Code copy
COPY /data/github.com/micro-lab/gohttp /data/gohttp

// Go build
WORKDIR /data/gohttp
RUN go get -d -v ./...
RUN go install -v ./cmd/$ServerName

// Run App
EXPOSE $ServerPort
CMD ["sh","-c","/go/bin/$ServerName"]

// 构建后查看镜像大小
$ docker images |grep micro
micro_server_b         latest              02e62a7f0f9e        2 days ago          409MB

// 实际的应用大小很小,主要内容在Go程序以及
/data/gohttp # ls -alh /go/bin/serverA
-rwxr-xr-x    1 root     root        7.2M Jul 26 09:50 /go/bin/serverA
/data/gohttp # du -sh /usr/local/go /go/pkg/
352.9M  /usr/local/go
38.1M   /go/pkg/

3.3.2. 构建环境和应用分离

基于多阶段构建Golang运行容器,将构建环境和应用分离

// 方式1:基于多阶段构建,构建环境没有充分利用
// go build envirment prepare
FROM golang:1.12-alpine as go-builder
LABEL Description="Build Golang Application" \
        Vendor="ACME Products" \
        Version="1.0"
ENV GOPROXY=https://goproxy.io
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
    apk update && apk add git
WORKDIR /data/gohttp
COPY /gohttp .
// go app comipler
RUN go install -v ./cmd/...

// Run go app server
FROM alpine:latest
LABEL Description="Golang Application Running"
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
    apk update && apk add --no-cache git curl tcpdump
COPY --from=go-builder /go/bin/* /data/go/bin/

// Runngin Envirment
ENV ServerName="ServerName"
EXPOSE 8851
EXPOSE 8852
CMD ["sh","-c","/data/go/bin/$ServerName"]

3.3.3. 利用建造者模式,将Golang编译环境独立出来

独立出来的编译环境,可以专职用于构建Golang应用

// 方式2:
// go build envirment prepare
FROM golang:1.12-alpine
LABEL Description="Build Golang Application" \
        Vendor="ACME Products" \
        Version="1.0"
ENV GOPROXY=https://goproxy.io
RUN sed -i 's/dl-cdn.alpinelinux.org/mirrors.aliyun.com/g' /etc/apk/repositories && \
    apk update && apk add git
VOLUME ["/go"]
WORKDIR /data/gohttp
COPY /gohttp .

3.3.4. 交叉编译应用程序

$ docker run --rm -it -v "$PWD":/usr/src/myapp -w /usr/src/myapp golang:1.8 bash
$ for GOOS in darwin linux; do
>   for GOARCH in 386 amd64; do
>     export GOOS GOARCH
>     go build -v -o myapp-$GOOS-$GOARCH
>   done
> done

3.4. 基于环境变量,传递给容器运行

// 运行ServerA
$ docker run -e ServerName=serverA -e ServerPort=8851 --rm -P micro_server
// 运行ServerB
$ docker run -e ServerName=serverB -e ServerPort=8852 --rm -P micro_server

$ curl localhost:32773
{"SrvName":"Server A","GoodId":7001100,"GoodSn":"SKU2001","Picture":["a.jpg","b.jpg"]}
$ curl localhost:32774
{"SrvName":"ServerB","UserId":100,"UserName":"Clark"}

3.5. 未完

剩余Docker-Composer、Docker-Swarm、Docker-Machine几块分开整理

4. 参考

  1. Docker概述:https://docs.docker.com/engine/docker-overview/
  2. 应用开发最佳实践:https://docs.docker.com/develop/dev-best-practices/
  3. 编写Dockerfiles的最佳实践:https://docs.docker.com/develop/develop-images/dockerfile_best-practices/
  4. 多阶段构建:https://docs.docker.com/develop/develop-images/dockerfile_best-practices/#use-multi-stage-builds
  5. golang docker hub: https://hub.docker.com/_/golang/