建立尽量小的 Docker 容器

注:本文由 Adriaan de Jonge 编写,本文的原文地址为 Create The Smallest Possible Docker Containerlinux

当咱们在使用 Docker 的时候,你会很快注意到你正在下载不少 MB 做为你的预先配置的容器。一个简单的 Ubuntu 容器很容易超过 200 MB,而且随着在上面安装软件,尺寸在逐渐增大。在某些状况下,你不须要任何事情都使用 Ubuntu 。例如,若是你只是简单的想运行一个 web 服务,使用 GO 编写的,没有必要围绕它使用任何工具。git

我一直在寻找尽量小的容器入手,而且发现了一个:github

docker pull scratch

scratch 镜像是完美的,真正的完美!它简洁,小巧以及快速。它不包含任何 bug,安全泄漏,慢的代码或是技术债务。这是由于它是一个空的镜像。除了一点由 Docker 加入的元数据。事实上,你可使用以下命令按照 Docker 文档描述的那样建立一个本身的 scratch 镜像。golang

tar cv --files-from /dev/null | docker import - scratch

因此这可能就是最小的 Docker 镜像。web

或者咱们能够说说关于这个的更多东西?好比,你怎样使用 scratch 镜像。这给本身带来了一些挑战。docker

为 scratch 镜像建立内容

咱们能够在一个空镜像中运行什么?一个没有依赖的可执行程序。你是否有没有依赖的可执行程序?shell

我过去经常使用 Python,Java 和 Javascript 编写代码。每个这样的语言/平台都须要一个运行时的安装。最近,我开始涉及 Go(或是 golang 若是你喜欢)平台。看起来 Go 是静态链接的。所以我尝试编译一个简单的 web 服务输出 Hello World 而且运行在 scratch 容器中。下面是这个 Hello World web 服务的代码:安全

package main

import (
    "fmt"
    "net/http"
)

func helloHandler(w http.ResponseWriter, r *http.Request) {
    fmt.Fprintln(w, "Hello World from Go in minimal Docker container")
}

func main() {
    http.HandleFunc("/", helloHandler)

    fmt.Println("Started, serving at 8080")
    err := http.ListenAndServe(":8080", nil)
    if err != nil {
        panic("ListenAndServe: " + err.Error())
    }
}

明显地,我不能在 scratch 容器中编译个人 web 服务,由于容器中没有 Go 编译器。正如我在 Mac 上工做,我也没法编译 Linux 的二进制文件同样(实际上,是能够在不一样的平台上交叉编译 Go 的源码的,但这会在另一篇博客中介绍)。bash

所以,我首先须要一个有 Go 编译器的 Docker 容器。让咱们开始:服务器

docker run -ti google/golang /bin/bash

在这个容器里面,我能够构建一个 Web 服务,经过我已经提交到一个 GitHub 仓库的代码。

go get github.com/adriaandejonge/helloworld

go get 命令是 go build 命令的变种,运行获取和构建远程的依赖。你能够运行可执行的结果:

$GOPATH/bin/helloworld

它工做了,可是这不是咱们想要的。咱们须要 hello world 容器运行在 scratch 容器里面。所以,实际上,咱们须要一个 Dockerfile :

FROM scratch
ADD bin/helloworld /helloworld
CMD ["/helloworld"]

而后启动它,不幸的是,咱们开始 google/golang 容器的这个方法, 没有办法构建这个 Dockerfile 。所以,首先,咱们须要一种方法从这个容器内部访问到 Docker。

从 Docker 内部调用 Docker

当你使用 Dokcer 时,你早晚会遇到须要从 Docker 内部访问 Docker。能够有多种方法实现它。你可使用递归和在 Docker 中运行 Docker。尽管如此,这样看起来会很复杂而且致使容器很大。你还可使用一些额外的命令选项在实例外访问 Docker 服务器:

docker run -v /var/run/docker.sock:/var/run/docker.sock -v $(which docker):$(which docker) -ti google/golang /bin/bash

在你继续前,你从新运行 Go 编译器,因为在重启动过程当中 Docker 忘记了咱们之前编译过。

go get github.com/adriaandejonge/helloworld

当咱们启动这个容器, -v 参数在 Docker 容器中建立一个卷而且容许你从 Docker 的机器提供一个文件做为输入。/var/run/docker.sock 是 UNIX socket,经过这个容许你访问 Docker 服务。 (which docker) 部分是一个很是聪明的方法,它提供了一个在 容器中的 Docker 可执行文件的路径,而不是硬编码。尽管如此,当你在 Mac 上经过 boot2docker 使用这个命令的时候须要当心。若是 Docker 的可执行文件与 boot2docker 虚拟机的在不一样的位置,将致使不匹配。所以,你或许想使用 /usr/local/bin/docker 硬编码的方式替换 $(which docker),若是你运行在不一样的系统,/var/run/docker.sock 有在不一样位置的机会,你须要作相应的调整。

如今你能够在 google/golang 容器的 $GOPATH 目录使用 Dockerfile ,在这个示例中指向 /gopath。实际上,我已经在 github 上检查过了这个 Dockerfile,所以,你能够从 Go build 目录复制它到所需的位置,像这样:

cp $GOPATH/src/github.com/adriaandejonge/helloworld/Dockerfile $GOPATH

你须要复制这个做为二进制的编译文件,如今位于 $GOPATH/bin,而且它不可能从父目录包含文件当构建一个 Dockerfile 的时候。所以复制后,下一步是:

docker build -t adejonge/helloworld $GOPATH

全部的都完成之后, Docker 给出以下响应:

Successfully built 6ff3fd5a381d

容许你运行这个容器:

docker run -ti --name hellobroken adejonge/helloworld

可是不幸的是, Docker 此次响应以下:

2014/07/02 17:06:48 no such file or directory

那么究竟是怎么回事?咱们在 scratch 容器中有可执行的静态连接。难道咱们犯了一个错误?

事实证实,Go 不是静态连接库。或者至少不是全部的库。在 Linux 下,咱们可使用 ldd 命令来看到动态连接库:

ldd $GOPATH/bin/helloworld

获得以下响应:

linux-vdso.so.1 => (0x00007fff039fe000)
libpthread.so.0 => /lib/x86_64-linux-gnu/libpthread.so.0 (0x00007f61df30f000)
libc.so.6 => /lib/x86_64-linux-gnu/libc.so.6 (0x00007f61def84000)
/lib64/ld-linux-x86-64.so.2 (0x00007f61df530000)

所以,在咱们运行咱们的 web 服务以前,我须要告诉 go 编译器实际的静态连接。

建立在 Go 中的可执行静态连接

为了建立可执行的静态连接,咱们须要告诉 Go 使用 cgo 编译器而不是 go 编译器。命令以下:

CGO_ENABLED=0 go get -a -ldflags '-s' github.com/adriaandejonge/helloworld

CGO_ENABLED 环境变量告诉 Go 使用 cgo 编译器而不是 go 编译器。-a 参数告诉 GO 重薪构建全部的依赖。不然的话你将以动态连接依赖结束。最后的 -ldflags '-s' 参数是一个很是好的扩展。它大概下降了可执行文件 50% 的文件大小。你也能够不经过 cgo 使用这个。尺寸缩小是去除了调试信息的结果。

为了肯定,运行 ldd 命令:

ldd $GOPATH/bin/helloworld

返回是:

not a dynamic executable

你也能够从新运行步骤,围绕着从 scratch 建立 Docker 容器的可执行文件。

docker build -t adejonge/helloworld $GOPATH

若是一切顺利,Docker 将响应以下:

Successfully built 6ff3fd5a381d

容许你运行这个容器:

docker run -ti --name helloworld adejonge/helloworld

响应以下:

Started, serving at 8080

到目前为止,有许多手动的步骤和不少错误的地方。让咱们退出 google/golang 容器而且从周边服务器继续:

<Press Ctrl-C>
exit

你能够检查 Docker 容器和镜像存在不存在:

docker ps -a
docker images -a

你可使用以下命令清理:

docker rm -f helloworld
docker rmi -f adejonge/helloworld

建立一个 Docker 容器来建立一个 Docker 容器

目前为止,咱们花了那么多步骤,咱们还能够记录在 Dockerfile 中而且 Docker 会为咱们作这些工做:

FROM google/golang
RUN CGO_ENABLED=0 go get -a -ldflags '-s' github.com/adriaandejonge/helloworld
RUN cp /gopath/src/github.com/adriaandejonge/helloworld/Dockerfile /gopath
CMD docker build -t adejonge/helloworld gopath

我在 一个单独的称为 adriaandejonge/hellobuild 的 GitHub 仓库检查了 Dockerfile。它可使用下面的命令构建:

docker build -t adejonge/hellobuild github.com/adriaandejonge/hellobuild

提供 -t 参数命名 adejonge/hellobuild 镜像而且它的最新的隐式的标签。这些名字让你之后更容易去除镜像。下一步,你可使用就像咱们在这篇文章前面看到的那样提供一个参数从这个镜像中建立一个容器:

docker run -v /var/run/docker.sock:/var/run/docker.sock -v $(which docker):$(which docker) -ti --name hellobuild adejonge/hellobuild

提供 --name hellobuild 参数使得在运行后更容易移除容器。事实上,你能够这样作,由于运行这个命令后,你已经建立了一个 adejonge/helloworld 镜像:

docker rm -f hellobuild
docker rmi -f adejonge/hellobuild

如今你能够建立一个基于 adejonge/helloworld 镜像的名为 helloworld 的新容器,就像你之前作的那样:

docker run -ti --name helloworld adejonge/helloworld

由于全部的这些步骤都是从相同的命令中运行,不须要在 Docker 中打开一个 bash shell 。你能够把这些步骤添加进一个 bash 脚本,自动运行它,为了使你方便,我已经把这些脚本加入了 hellobuild GitHub 仓库

另外,若是你想尝试一个尽量小的容器,可是又不想遵循博客中的步骤,你可使用我检入进 Docker Hub repository 的预先构建好的镜像。

docker pull adejonge/helloworld

使用 docker images -a ,你能够看到大小是 3.6MB。固然,若是你成功建立一个比我使用 Go 编写的 web 服务还小的可执行文件,你可使得它更小。使用 C 语言或者是汇编,你能够这样作到。尽管如此,你不可能使得它比 scratch 镜像还小

扩展阅读

相关文章
相关标签/搜索