Docker 学习后记
2016 3 7 03:16 AM 5611次查看
由于时间有限,我基本上是按自己的理解写的,所以难免会有误,最好结合官方文档看吧(其实官方文档也很不详细)。另外,Docker 变化挺频繁的,我目前用的是 1.10 版,未来可能不完全适用。
首先需要介绍一些名词。
- Docker:根据 Docker 官网的介绍,它是一个把应用和它的依赖打包在一个标准化的单元里的技术。
听上去好像还没什么概念,其实就是把应用的代码、运行时、系统工具、系统库和操作系统等资源打包在一起,然后就能一起部署了。因为所有的东西都自带了,部署时就不需要再安装了,也就避免了遇到一些奇怪的安装问题。同时,由于它是标准化的,每个单元需要的资源都是相同的,所以规划时可以很轻松地按 CPU、内存、硬盘等资源分配,更细地控制资源的粒度,而不用担心资源的闲置。 - Docker container(容器):前面所说的应用就是在它内部跑的。它有自己完整的文件系统和操作系统,反正应用需要的一切本地资源都有。
- Docker image(镜像):应用代码和各种库和文件会被打包成一个文件,这就是 image 了。它还可以设置环境变量、运行的入口等元信息。它需要被加载到容器里才能运行。
- Docker daemon:监听客户端请求,提供容器和镜像的管理功能。
- Docker client:与 daemon 通信,完成管理功能。
- Docker Engine:由 Docker daemon 及其 API 组成的一个服务端应用。
- Docker Machine:可以在虚拟机上安装 Docker Engine 的工具。
- Docker registry:存放 image 的地方。最大的一个是 Docker Hub,可以很容易地知道别人的镜像是怎么做的。
其他我就不说了,反正后面会继续介绍到。
接着就开始使用 Docker 吧。
虽然按官方文档上的步骤来的话,需要在本地安装 Docker Machine,不过 OS X 和 Windows 都需要装虚拟机才行,而且即使装好了,下载速度也十分忧伤(我花了一个小时也没下载成功一个 5 MB 的 image),所以建议直接跳过这步。
Carina 目前提供免费的 Docker 容器运行服务,12 核 CPU,4.2 GB 内存,关键是国外的服务器从 Docker Hub 拉镜像飞快,比自己折腾好多了。
注册完了就可以创建一个 cluster(集群)了,创建完点「Get Access」按钮,下载用来连接 daemon 的配置和证书文件,解压后运行
source docker.env
就配置好了(Windows 自己看说明吧)。然后是安装 Docker client。OS X 可以直接用
brew install docker
,其他的看官方文档吧。于是,准备工作就做好了。
先来看看我们的 Docker 信息:
docker info
里面会列出 container 和 image 的数量,宿主机器的操作系统,还有 CPU 核数与内存大小等。然后起个 Ubuntu 的容器登上去看看吧:
docker run -t -i ubuntu /bin/bash
这里的 run
是指运行一个容器;-t
是创建一个伪 TTY,-i
表示保持 STDIN 打开,这 2 个参数的作用是让终端可用;ubuntu
是要用的镜像名,没有的话会自动去 registry 下载,因为没有加标签,所以会自动使用 ubuntu:latest
;/bin/bash
是要运行的命令。执行完后,就进入这个容器的 bash 了,可以看到用户是
root
,路径是 /
。但这和宿主机器或其他容器是隔离的,所以你可以随意乱改里面的文件。执行
exit
或按 ctrl+d 就可以退出了。再来看看有哪些容器在运行:
docker ps -a
这里的 ps
和本地的 ps 命令类似,用于列出容器;-a
表示列出所有容器。可以看到之前 Ubuntu 的那个容器状态变成 Exited 了。它前面还有一串随机的字符串(我这里是
1427dc4db2a2
),是那个容器的 ID。于是再启动那个容器吧:
docker start 1427dc4db2a2
然后再次登上去(不能再用 run
命令了,否则会新建一个容器):docker attach 1427dc4db2a2
这两条命令可以用一条代替:docker start -a -i 1427dc4db2a2
其中,-a
表示 attach,-i
表示开启 STDIN。如果想连多个 shell 的话,
attach
命令会卡住,可以用 exec
命令让这个容器再运行一个 bash:docker exec -i -t 1427dc4db2a2 /bin/bash
如果退出 attach 的那个 shell 的话,容器就会停止了,exec 执行的 bash 也会被终止。现在这个 Ubuntu 的容器对我来说没用了,可以删掉了:
docker rm 1427dc4db2a2
如果你玩得很 high,发现有一大堆退出的容器,一个个删除就太麻烦了,可以这样删:docker rm $(docker ps -q -f status=exited)
接下来看看更常见的用法,让一个容器长期运行。毕竟 Docker 主要用来跑服务器应用,要是退出登录就自动停了,那就没法服务了。
普通的服务没啥好玩的,我们来起个 Shadowsocks 服务吧:
docker run -d -p 8888:9999 oddrationale/docker-shadowsocks -s 0.0.0.0 -p 9999 -k password -m aes-256-cfb
有没有吓尿,一行代码就跑起来了,比自己弄个 VPS 快捷多了。顺便解释下,这里的
-d
表示在后台运行,也就是不会自动退出;-p 8888:9999
表示把容器的 9999
端口映射到宿主机的 8888
端口;oddrationale/docker-shadowsocks
是镜像的名字,oddrationale
是作者的名字,可以在 Docker Hub 找到这个 image;再后面则是 Shadowsocks 的参数,就不解释了。Docker 的基本操作大概就这么多了。
接下来开始进阶,学习如何创建自己的 image。
因为我正好在做 Doodle 的 Docker 部署方案,所以就拿它下手了。
创建一个镜像最好的办法是写一个
Dockerfile
,里面可以描述安装和运行应用的步骤。折腾完后大致如下:
# 使用 ubuntu:14.04 镜像作为基础镜像,14.04 是镜像的 tag,不指定会用 latest,建议指定
FROM ubuntu:14.04
MAINTAINER keakon <keakon@gmail.com>
# 创建一个环境变量,因为下面经常会用到
ENV DOODLE_PATH /data/doodle
# 安装依赖的库
RUN apt-get update && apt-get upgrade -y
RUN apt-get install -y git wget python build-essential python-dev libcurl4-openssl-dev
# clone 仓库
RUN git clone https://github.com/keakon/Doodle.git $DOODLE_PATH
# cd 到工作目录
WORKDIR $DOODLE_PATH
# 运行安装脚本
RUN scripts/buildout.sh
# 设置运行入口
ENTRYPOINT ["bin/doodle"]
把它保存成
Dockerfile
文件后,就可以开始构建了:docker build -t doodle .
其中,-t doodle
表示构建完的镜像名为 doodle
,当然也可以写成 keakon/doodle:2.0
这样的形式。如果你很不幸地发现需要花很长时间,那么你可能是把当前文件夹里的文件都上传给 Docker Engine 了。于是赶紧取消掉,创建一个空的文件夹,把
Dockerfile
扔进来,再 build 一次吧。漫长的等待后,镜像就生成好了。列出我们现有的镜像看看:
docker images
可以看到我们的镜像建好了,镜像大小快 500 MB 了……先不管这个,我们运行下试试:
docker run -d -p 8080:8080 doodle
然后访问下首页(http://宿主机 IP:8080/),发现挂掉了,原因是没有 Redis。在 Docker 上跑一个 Redis 也是很简单的,不过我不希望它暴露在公网上,所以就把它放在一个私有网络里吧:
docker network create doodle
docker run -d --name redis --net doodle redis
docker run -d --name doodle --net doodle -p 8080:8080 --env REDIS_HOST=redis doodle
这里我创建了一个叫 doodle
的网络,--name
后面指定的是机器别名,--net
后面指定的是网络名,--env REDIS_HOST=redis
表示设置了一个 REDIS_HOST=redis
的环境变量,然后我在 Doodle 的代码中会读取这个环境变量,作为 Redis 的 host。这样处理后,应用就成功跑起来了。
接着修改一些代码,再部署一次镜像试试。结果很快就执行完了,镜像也没任何变化,每一步都显示
Using cache
。原因是 Docker 在构建镜像时,会把每一步的结果缓存起来,下次构建时,如果这一步的命令没有变化,就直接使用缓存。如果要取消这个缓存机制,加上 --no-cache
参数就行了,但这也导致了构建镜像时需要从头开始,花费很长的时间。要优雅地解决这个问题,就得看看文档了。读完发现 Dockerfile 里可以使用
ADD
和 COPY
指令添加文件,如果和之前构建的镜像的文件发生了变化,就会忽略缓存了。看到这里我才发现为啥构建镜像时,会自动把这个文件夹里的文件都上传给 Docker Engine,原因是上传后它才能被 ADD
到镜像里。所以如果网络快的话,直接使用项目的根目录作为构建的目录,设置 ADD . $DOODLE_PATH
即可;但我这破网还是不想了,考虑到 Github 有个 API 可以拿到提交记录,于是我把这个网址 ADD
进来,只要它的内容变化了(提交了新版本),就会构建新的镜像:# ...
RUN git clone https://github.com/keakon/Doodle.git $DOODLE_PATH
WORKDIR $DOODLE_PATH
# 用于区分缓存
ADD https://api.github.com/repos/keakon/Doodle/commits /tmp/commits
# 之前的 clone 可能被缓存了,需要更新到最新的代码
RUN git fetch && git reset --hard origin/master
RUN scripts/buildout.sh
# ...
而为了更好的利用缓存,建议前面的语句尽量是不会更改的。而操作系统的镜像要指定版本号,也是为了避免 latest 升级时,不会相应地变化。现在一切都很好,除了镜像太大了这点……
其实知乎也没解决这个问题,知乎日报 / 读读日报的镜像压缩后大概还有 500+ MB,而且构建镜像的机器和生产环境在不同的机房,传输时间基本在 5 分钟以上……
于是我寻找了一下更小的镜像,毕竟 Ubuntu 本身就占了 188 MB,很不划算。搜了一下后发现 Alpine 这个 5 MB 大的 Linux 镜像,于是折腾了一番后,写出这样一个
Dockerfile
:FROM alpine:3.3
MAINTAINER keakon <keakon@gmail.com>
ENV DOODLE_PATH /data/doodle
RUN apk update && apk upgrade
RUN apk add --no-cache git wget gcc python python-dev musl-dev curl-dev
RUN git clone https://github.com/keakon/Doodle.git $DOODLE_PATH
WORKDIR $DOODLE_PATH
ADD https://api.github.com/repos/keakon/Doodle/commits /tmp/commits
RUN git fetch && git reset --hard origin/master
RUN if [ ! -f bin/buildout ]; then \
mkdir downloads -p; \
wget https://bootstrap.pypa.io/bootstrap-buildout.py -O bootstrap-buildout.py && python bootstrap-buildout.py --setuptools-to-dir downloads; \
fi
RUN bin/buildout -N
ENTRYPOINT ["bin/doodle"]
需要注意的是,Alpine 的包管理用的是 apk;此外,它默认不带 bash ,用的是 sh。现在,这个镜像只有 180 MB 了,而且也能正常运行,成果显著。
接着让我们再删除一些东西吧,毕竟安装的 C 库和工具中,大部分是临时性使用的,删掉也不影响运行:
# ...
RUN bin/buildout -N
RUN apk del git wget gcc python-dev musl-dev
RUN rm -rf /var/cache/apk/*
ENTRYPOINT ["bin/doodle"]
再看下生成的镜像,居然还稍微变大了点……原来 Docker 的镜像是有层级的,Dockerfile 中的每条指令都会创建一个新的层,用来保存和前一层不同的地方。最终,整个镜像文件的大小是所有层的大小加起来的。因此,即使后面的层删除了文件,也不会导致镜像文件变小。
搜了一下后发现,把镜像导出再导入,就会去掉这些层级了:
docker export 92ffa1fef80d | docker import --change='ENTRYPOINT ["/data/doodle/bin/doodle"]' - doodle:flat
其中,92ffa1fef80d
是一个正在运行 doodle:alpine 的容器 ID。--change
是为了设置入口进程,因为 export 时,Dockerfile 中的指令都丢失了,只剩文件系统了。不过这条命令需要把镜像文件下载回来,然后再上传上去,速度实在感人,所以我还是放弃了。
后来灵机一动,为啥不在一个容器中构建这个镜像呢?
于是起了一个 Ubuntu 的容器,把证书等 ADD 进去,然后装上 docker client,就可以连上 docker daemon 了。再在容器里运行上面的命令,果然很快就构建好了。
可是运行的时候却提示:
docker: Error response from daemon: 500 Internal Server Error: No command specified.但是手动指定 ENTRYPOINT 就能正常运行:
docker run -d --name doodle --net doodle -p 8080:8080 --env REDIS_HOST=redis doodle:flat /data/doodle/bin/doodle
于是猜测 import --change
有 bug,把这个新的容器提交一下试试:docker commit --change='ENTRYPOINT ["/data/doodle/bin/doodle"]' 4e71cf7bb527 doodle:flat
其中,4e71cf7bb527
是新的容器 ID。这样提交之后,doodle:flat
这个镜像终于可用了,大小约为 70 MB。如果更狠一点的话,应该还有很多文件可以删掉,不过这里我就懒得去清理了。
而如果要让这个过程可被复用的话,可以把这些操作写成一个脚本,放到那个构建镜像用的容器中。
这脚本我也懒得写了,应该有很多第三方的镜像瘦身的工具可以替代。真想吐槽一句,这么重要的一个功能,Docker 团队却不愿意官方提供,让我对它非常失望。
顺便提一下,容器是临时性的,它的文件系统和宿主机的文件系统是没关系的,如果不提交到镜像里的话,里面的文件在容器销毁时就丢失了。
如果要持久保存文件的话,需要用
VOLUME
指令设置一些路径对应到宿主机的文件系统,让其中的文件可以在多个容器间共用和重用。最后,之前我在用 Redis 时,手动设置了环境变量。其实可以用
--link
参数来自动设置环境变量,详情可以看文档。
向下滚动可载入更多评论,或者点这里禁止自动加载。