创建尽可能小的 Docker 容器

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

我一直在寻找尽可能小的容器入手,并且发现了一个:


  1. docker pull scratch

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


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

所以这可能就是最小的 Docker 镜像。

或者我们可以说说关于这个的更多东西?比如,你怎样使用 scratch 镜像。这给自己带来了一些挑战。

为 scratch 镜像创建内容

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

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


  1. package main
  2. import (
  3. "fmt"
  4. "net/http"
  5. )
  6. func helloHandler(w http.ResponseWriter, r *http.Request) {
  7. fmt.Fprintln(w, "Hello World from Go in minimal Docker container")
  8. }
  9. func main() {
  10. http.HandleFunc("/", helloHandler)
  11. fmt.Println("Started, serving at 8080")
  12. err := http.ListenAndServe(":8080", nil)
  13. if err != nil {
  14. panic("ListenAndServe: " + err.Error())
  15. }
  16. }

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

因此,我首先需要一个有 Go 编译器的 Docker 容器。让我们开始:


  1. docker run -ti google/golang /bin/bash

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


  1. go get github.com/adriaandejonge/helloworld

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


  1. $GOPATH/bin/helloworld

它工作了,但是这不是我们想要的。我们需要 hello world 容器运行在 scratch 容器里面。因此,实际上,我们需要一个 Dockerfile :


  1. FROM scratch
  2. ADD bin/helloworld /helloworld
  3. CMD ["/helloworld"]

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

从 Docker 内部调用 Docker

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


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

在你继续前,你重新运行 Go 编译器,由于在重启动过程中 Docker 忘记了我们以前编译过。


  1. 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 目录复制它到所需的位置,像这样:


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

你需要复制这个作为二进制的编译文件,现在位于 $GOPATH/bin,并且它不可能从父目录包含文件当构建一个 Dockerfile 的时候。因此复制后,下一步是:


  1. docker build -t adejonge/helloworld $GOPATH

所有的都完成以后, Docker 给出如下响应:


  1. Successfully built 6ff3fd5a381d

允许你运行这个容器:


  1. docker run -ti --name hellobroken adejonge/helloworld

但是不幸的是, Docker 这次响应如下:


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

那么到底是怎么回事?我们在 scratch 容器中有可执行的静态链接。难道我们犯了一个错误?

事实证明,Go 不是静态链接库。或者至少不是所有的库。在 Linux 下,我们可以使用 ldd 命令来看到动态链接库:


  1. ldd $GOPATH/bin/helloworld

得到如下响应:


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

因此,在我们运行我们的 web 服务之前,我需要告诉 go 编译器实际的静态链接。

创建在 Go 中的可执行静态链接

为了创建可执行的静态链接,我们需要告诉 Go 使用 cgo 编译器而不是 go 编译器。命令如下:


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

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

为了确定,运行 ldd 命令:


  1. ldd $GOPATH/bin/helloworld

返回是:


  1. not a dynamic executable

你也可以重新运行步骤,围绕着从 scratch 创建 Docker 容器的可执行文件。


  1. docker build -t adejonge/helloworld $GOPATH

如果一切顺利,Docker 将响应如下:


  1. Successfully built 6ff3fd5a381d

允许你运行这个容器:


  1. docker run -ti --name helloworld adejonge/helloworld

响应如下:


  1. Started, serving at 8080

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


  1. <Press Ctrl-C>
  2. exit

你可以检查 Docker 容器和镜像存在不存在:


  1. docker ps -a
  2. docker images -a

你可以使用如下命令清理:


  1. docker rm -f helloworld
  2. docker rmi -f adejonge/helloworld

创建一个 Docker 容器来创建一个 Docker 容器

目前为止,我们花了那么多步骤,我们还可以记录在 Dockerfile 中并且 Docker 会为我们做这些工作:


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

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


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

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


  1. 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 镜像:


  1. docker rm -f hellobuild
  2. docker rmi -f adejonge/hellobuild

现在你可以创建一个基于 adejonge/helloworld 镜像的名为 helloworld 的新容器,就像你以前做的那样:


  1. docker run -ti --name helloworld adejonge/helloworld

因为所有的这些步骤都是从相同的命令中运行,不需要在 Docker 中打开一个 bash shell 。你可以把这些步骤添加进一个 bash 脚本,自动运行它,为了使你方便,我已经把这些脚本加入了 hellobuild GitHub 仓库

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


  1. docker pull adejonge/helloworld

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

原文发布时间为:2015-06-09

本文来自合作伙伴“Linux中国”

时间: 2024-09-08 18:40:52

创建尽可能小的 Docker 容器的相关文章

【教程】如何创建尽可能小的Docker容器

本文讲的是[教程]如何创建尽可能小的Docker容器,[编者的话]本文作者以一个使用Go语言编写的Web服务为例,重点介绍了如何通过Scratch创建一个尽可能小的Docker容器.在尝试过程中,作者也发现了很多问题,也逐一得到解决,感兴趣的读者一定要看看作者解决问题的思路.本文看点包括如何从Docker内部调用Docker.创建Docker容器的Docker容器.Go语言创建静态链接的可执行文件. 当在使用Docker的时候,如果想使用预先配置好的容器,就需要下载很大的镜像包.一个简单的Ubu

Docker容器:更小不一定更好

本文讲的是Docker容器:更小不一定更好,[编者的话]按正常逻辑来说,我们应该选择体积较小的Docker容器.然而事实是,体积小却并不一定能带来性能上的优势.本文将介绍一个使用了一个体积稍大一点的容器,从而将性能提高30倍以上的例子. 按道理来说,我们应该选择体积较小的Docker容器.然而事实是,体积小却并不一定能带来性能上的优势.本文将介绍一个使用了一个体积稍大一点的容器,从而将性能提高30倍以上的例子. 摘要 当使用grep来处理大量的数据的时候,busybox中带的grep速度慢的让人

基础的 Docker 容器网络命令

各位好,今天我们将学习一些Docker容器的基础命令.Docker 是一个开源项目,提供了一个可以打包.装载和运行任何应用的轻量级容器的开放平台.它没有语言支持.框架和打包系统的限制,从小型的家用电脑到高端服务器,在何时何地都可以运行.它可以使部署和扩展web应用程序.数据库和后端服务像搭积木一样容易,而不依赖特定技术栈或提供商.Docker适用于网络环境,它正应用于数据中心.ISP和越来越多的网络服务. 因此,这里有一些你在管理Docker容器的时候会用到的一些命令. 1. 找到Docker接

如何在Docker容器中运行GUI程序

如何在Docker容器中运行GUI程序 各位,今天我们将学习如何在Docker之中运行GUI程序.我们可以轻易地在Docker容器中运行大多数GUI程序且不出错.Docker是一个开源项目,提供了一个打包.分发和运行任意程序的轻量级容器的开放平台.它没有语言支持.框架或者打包系统的限制,并可以运行在任何地方.任何时候,从小型的家用电脑到高端的服务器都可以运行.这让人们可以打包不同的包用于部署和扩展网络应用,数据库和后端服务而不必依赖于特定的栈或者提供商. 下面是我们该如何在Docker容器中运行

使用OpenStack管理Docker容器(一)

本文讲的是使用OpenStack管理Docker容器(一),[编者的话]本文将讲述如何使用OpenStack创建并管理Docker,有3种流行的使用方法,使用的分别是Nova Docker驱动,Heat Docker插件,以及Magnum.这篇文章分成2部分,第一部分,将主要介绍Nova Docker驱动的用法.第二部分,是关于Heat Docker插件和Magnum.这是序列文章的第一部分. 在这篇文章中,我将介绍一些不同的方法,这些方法是关于OpenStack如何创建,以及管理Docker容

微容器:更小的,更轻便的Docker容器

本文讲的是微容器:更小的,更轻便的Docker容器,[编者的话]本文介绍了微容器的概念和好处,并用一些例子介绍了如何构建微镜像,从scratch到Alpine Linux,并推荐了一些已有的基础微镜像,方便为几乎所有主流语言的应用构建微镜像.本文也指出了构建微镜像的基本原理:将构建时依赖和运行时依赖分开,构建时所用的镜像包含所有构建所用的工具,它可以比较大,但运行时的基础镜像应该仅包含运行时依赖.使用微容器,no going back! Docker 使你能把你的应用和应用的依赖打包到一个良好的

docker如何创建一个运行后台进程的容器并同时提供shell终端

只看标题还不是很明显,本文实现docker的这样一种比较常用的功能:通过docker run启动一个容器后,容器中已经运行了一个后台进程(这里以监听80端口的nginx为例),同时进入一个shell终端可供操作,而不受限于只能在前台运行nginx与运行shell终端之间的一种.这个例子实现了,那么其他类似的运行多任务docker就可以以此类推.另外本文还提供了一种在docker容器内部安装软件(vim)的方法,对于定制自己需要的镜像大有帮助. 你可能需要先阅读docker专题(2):docker

docker学习(5) 在mac中创建mysql docker容器

github上有一个专门的docker-libary项目,里面有各种各样常用的docker镜像,可以做为学习的示例,今天研究下其中mysql镜像的用法,国内镜像daocloud.io也能找到mysql的镜像,但根据其参考文档在mac上尝试了数次,将mysql数据库文件存储在mac本机时,启动总是报错,大意是docker容器运行时,容器的当前用户mysql,由于权限不足无法mac本机上创建文件,stackoverflow上有人解决了这个问题,参考其解决方法,重新整理了下Dockerfile文件,内

如何交互式地创建一个Docker容器

如何交互式地创建一个Docker容器 大家好,今天我们来学习如何使用一个docker镜像交互式地创建一个Docker容器.当我们从镜像中启动一个Docker进程,Docker就会获取该镜像及其父镜像,并重复这个过程,直到到达基础镜像.然后联合文件系统(UFS)会在其顶层添加一个读写层.读写层被称之为容器,它包含了一些关于父镜像信息及一些其他的信息,如唯一ID,网络配置和资源限制等.容器是有状态的,其状态可以从 运行态 切换到 退出态.一个处于 运行态的容器包含了在CPU上面运行的进程树,于其它在