[编者的话] 当文字偶遇代码,当程序插上了翅膀,让分享成为我们彼此沟通的语言。我们期待可以构建这样一个平台让开发者们看到你们的智慧,挖掘你们的才华,让彼此在开源的路上不再孤独。“DaoCloud分享写作计划”已全面启动,欢迎投稿。
作者: 刘思贤
关注Docker这项技术差不多一年多了,最近关于Docker的使用案例越来越多,于是就想动手实验下这项技术。自己整理了一个入门手册,分享给大家。
Docker是一种容器技术,它可以将应用和环境等进行打包,形成一个独立的,类似于iOS的APP形式的“应用”,这个应用可以直接被分发到任意一个支持Docker的环境中,通过简单的命令即可启动运行。Docker是一种最流行的容器化实现方案。和虚拟化技术类似,它极大的方便了应用服务的部署;又与虚拟化技术不同,它以一种更轻量的方式实现了应用服务的打包。使用Docker可以让每个应用彼此相互隔离,在同一台机器上同时运行多个应用,不过他们彼此之间共享同一个操作系统。Docker的优势在于,它可以在更细的粒度上进行资源的管理,也比虚拟化技术更加节约资源。
开始试验Docker之前,我们先来了解一下Docker的几个基本概念:
镜像:我们可以理解为一个预配置的系统光盘,这个光盘插入电脑后就可以启动一个操作系统。当然由于是光盘,所以你无法修改它或者保存数据,每次重启都是一个原样全新的系统。Docker里面镜像基本上和这个差不多。
容器:同样一个镜像,我们可以同时启动运行多个,运行期间的产生的这个实例就是容器。把容器内的操作和启动它的镜像进行合并,就可以产生一个新的镜像。
Docker基于 LXC 技术实现,依赖于Linux内核,所以Docker目前只能在Linux以原生方式运行。目前主要的Linux发行版在他们的软件仓库中内置了Docker:
Docker要求64位环境,这些操作系统下可以直接通过命令安装Docker,老一些操作系统Docker官方也提供了安装方案。下面的实验基于CentOS 7进行。关于其他版本操作系统上Docker的安装,请参考官方文档: https://docs.docker.com/installation/
使用yum从软件仓库安装Docker:
yum install docker
首先启动Docker的守护进程:
service docker start
如果想要Docker在系统启动时运行,执行:
chkconfig docker on
Docker使用类似git的方式管理镜像。通过基本的镜像可以定制创建出来不同种应用的Docker镜像。 Docker Hub 是Docker官方提供的镜像中心。在这里可以很方便地找到各类应用、环境的镜像。
由于Docker使用联合文件系统,所以镜像就像是夹心饼干一样一层层构成,相同底层的镜像可以共享。所以Docker还是相当节约磁盘空间的。要使用一 个镜像,需要先从远程的镜像注册中心拉取,这点非常类似git。
docker pull ubuntu
我们很容易就能从Docker Hub镜像注册中心下载一个最新版本的ubuntu镜像到本地。国内网络可能会稍慢,DaoCloud提供了Docker Hub的国内加速服务,可以尝试配置使用。
使用Docker最关键的一步就是从镜像创建容器。有两种方式可以创建一个容器:使用docker create命令创建容器,或者使用docker run命令运行一个新容器。两个命令并没有太大差别,只是前者创建后并不会立即启动容器。
以ubuntu为例,我们启动一个新容器,并将ubuntu的Shell作为入口:
docker run -it ubuntu:latest sh -c '/bin/bash'
这时候我们成功创建了一个Ubuntu的容器,并将当前终端连接为这个Ubuntu的bash shell。这时候就可以愉快地使用Ubuntu的相关命令了~可以快速体验一下。
参数-i表示这是一个交互容器,会把当前标准输入重定向到容器的标准输入中,而不是终止程序运行。-t指为这个容器分配一个终端。
好了,按Ctrl+D可以退出这个容器了。
在容器运行期间,我们可以通过docker ps命令看到所有当前正在运行的容器。添加-a参数可以看到所有创建的容器:
shell docker ps -a
[root@localhost ~]# docker ps -a CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES cb2b06c83a50 ubuntu:latest "sh -c /bin/bash" 7 minutes ago Exited (0) 7 seconds ago trusting_morse
每个容器都有一个唯一的ID标识,通过ID可以对这个容器进行管理和操作。在创建容器时,我们可以通过–name参数指定一个容器名称,如果没有指定系统将会分配一个,就像这里的“trusting_morse”(什么鬼TAT)。
当我们按Ctrl+D退出容器时,命令执行完了,所以容器也就退出了。要重新启动这个容器,可以使用docker start命令:
shell docker start -i trusting_morse
同样,-i参数表示需要交互式支持。
注意:每次执行docker run命令都会创建新的容器,建议一次创建后,使用docker start/stop来启动和停用容器。
在Docker容器运行期间,对文件系统的所有修改都会以增量的方式反映在容器使用的联合文件系统中,并不是真正的对只读层数据信息修改。每次运行容器对它的修改,都可以理解成对夹心饼干又添加了一层奶油。这层奶油仅供当前容器使用。当删除Docker容器,或通过该镜像重新启动时,之前的更改将会丢失。这样做并不便于我们持久化和共享数据。Docker的数据卷这个东西可以帮到我们。
在创建容器时,通过 -v
参数可以指定将容器内的某个目录作为数据卷加载:
docker run -it -v /home/www ubuntu:latest sh -c '/bin/bash'
在容器中会多一个/home/www挂载点,在这个挂载点存储数据会绕过联合文件系统。我们可以通过下面的命令来找到这个数据卷在主机上真正的存储位置:
shell docker inspect -f {{.Volumes}} trusting_morse
你会看到输出了一个指向到 /var/lib/docker/vfs/dir/...
的目录。cd进入后你会发现在容器中对 /home/www
的读写创建,都会反映到这儿。事实上, /home/www
就是挂载自这个位置。
有时候,我们需要将本地的文件目录挂载到容器内的位置,同样是使用数据卷这一个特性, -v
参数格式为:
shell docker run -it -v [host_dir]:[container_dir]
host_dir
是主机的目录, container_dir
是容器的目录。
容器和容器之间是可以共享数据卷的,我们可以单独创建一些容器,存放如数据库持久化存储、配置文件一类的东西,然而这些容器并不需要运行。
shell docker run --name dbdata ubuntu echo "Data container."
在需要使用这个数据容器的容器创建时–volumes-from [容器名]的方式来使用这个数据共享容器。
Docker容器内的系统工作起来就像是一个虚拟机,容器内开放的端口并不会真正开放在主机上。可以使我们的容器更加安全,而且不会产生容器间端口的争用。想要将Docker容器的端口开放到主机上,可以使用类似端口映射的方式。
在Docker容器创建时,通过指定-p参数可以暴露容器的端口在主机上:
shell docker run -it -p 22 ubuntu sh -c '/bin/bash'
现在我们就将容器的22端口开放在了主机上,注意主机上对应端口是自动分配的。如果想要指定某个端口,可以通过-p [主机端口]:[容器端口]参数指定。
容器和容器之间想要网络通讯,可以直接使用–link参数将两个容器连接起来。例如WordPress容器对some-mysql的连接:
shell docker run --name some-wordpress --link some-mysql:mysql -p 8080:80 -d wordpress
通过Docker打包的应用,对外就像是一个密闭的exe可执行文件。有时候我们希望Docker能够使用一些外部的参数来使用容器,这时候参数可以通过环境变量传递进去,通常情况下用来传递比如MySQL数据库连接这种的东西。环境变量通过 -e
参数向容器传递:
shell docker run --name some-wordpress -e WORDPRESS_DB_HOST=10.1.2.3:3306 / -e WORDPRESS_DB_USER=... -e WORDPRESS_DB_PASSWORD=... -d wordpress
关于Docker到现在就有了一个基本的认识了。接下来我会给大家介绍如何创建镜像。
Docker强大的威力在于可以把自己开发的应用随同各种依赖环境一起打包、分发、运行。要创建一个新的Docker镜像,通常基于一个已有的Docker镜像来创建。Docker提供了两种方式来创建镜像:把容器创建为一个新的镜像、使用Dockerfile创建镜像。
为了创建一个新的镜像,我们先创建一个新的容器作为基底:
shell docker run -it ubuntu:latest sh -c '/bin/bash'
现在我们可以对这个容器进行修改了,例如我们可以配置PHP环境、将我们的项目代码部署在里面等:
shell apt-get install php # some other opreations ...
当执行完操作之后,我们按Ctrl+D退出容器,接下来使用 docker ps -a
来查找我们刚刚创建的容器ID:
shell docker ps -a
可以看到我们最后操作的那个ubuntu容器。这时候只需要使用 docker commit
即可把这个容器变为一个镜像了:
docker commit 8d93082a9ce1 ubuntu:myubuntu
这时候docker容器会被创建为一个新的Ubuntu镜像,版本名称为 myubuntu
。以后我们可以随时使用这个镜像来创建容器了,新的容器将自动包含上面对容器的操作。
如果我们要在另外一台机器上使用这个镜像,可以将一个镜像导出:
shell docker save -o myubuntu.tar.gz ubuntu:myubuntu
现在我们可以把刚才创建的镜像打包为一个文件分发和迁移了。要在一台机器上导入镜像,只需要:
shell docker import myubuntu.tar.gz
这样在新机器上就拥有了这个镜像。
注意:通过导入导出的方式分发镜像并不是Docker的最佳实践,因为我们有Docker Hub。
Docker Hub提供了类似GitHub的镜像存管服务。一个镜像发布到Docker Hub不仅可以供更多人使用,而且便于镜像的版本管理。关于Docker Hub的使用,之后我会单独写一篇文章展开介绍。另外,在一个企业内部可以通过自建 Docker Registry 的方式来统一管理和发布镜像。将Docker Registry集成到版本管理和上线发布的工作流之中,还有许多工作要做,在我整理出最佳实践后会第一时间分享。
使用命令行的方式创建Docker镜像通常难以自动化操作。在更多的时候,我们使用Dockerfile来创建Docker镜像。Dockerfile是一个纯文本文件,它记载了从一个镜像创建另一个新镜像的步骤。撰写好Dockerfile文件之后,我们就可以轻而易举的使用docker build命令来创建镜像了。
Dockerfile非常简单,仅有以下命令在Dockerfile中常被使用:
命令 | 参数 | 说明 |
---|---|---|
# | - | 注释说明 |
FROM | <image>[:<tag>] | 从一个已有镜像创建,例如ubuntu:latest |
MAINTAINER | Author <some-one@example.com> | 镜像作者名字,如Max Liu <some-one@example.com> |
RUN | <cmd>或者[‘cmd1′, ‘cmd2’…] | 在镜像创建用的临时容器里执行单行命令 |
ADD | <src> <dest> | 将本地的<src>添加到镜像容器中的<dest>位置 |
VOLUME | <path>或者[‘/var’, ‘home’] | 将指定的路径挂载为数据卷 |
EXPOSE | <port> [<port>…] | 将指定的端口暴露给主机 |
ENV | <key> <value> 或者 <key> = <value> | 指定环境变量值 |
CMD | [“executable”,”param1″,”param2″] | 容器启动时默认执行的命令。注意一个Dockerfile中只有最后一个CMD生效。 |
ENTRYPOINT | [“executable”, “param1″, “param2″] | 容器的进入点。 |
下面是一个Dockerfile的例子:
# This is a comment FROM ubuntu:14.04 MAINTAINER Kate Smith <ksmith@example.com> RUN apt-get update && apt-get install -y ruby ruby-dev RUN gem install sinatra
这里其他命令都比较好理解,唯独CMD和ENTRYPOINT我需要特殊说明一下。CMD命令可用指定Docker容器启动时默认的命令,例如我们上面例子提到的 docker run -it ubuntu:latest sh -c '/bin/bash'
。其中 sh -c '/bin/bash'
就是通过手工指定传入的CMD。如果我们不加这个参数,那么容器将会默认使用CMD指定的命令启动。ENTRYPOINT是什么呢?从字面看是进入点。没错,它就是进入点。 ENTRYPOINT用来指定特定的可执行文件、Shell脚本,并把启动参数或CMD指定的默认值,当作附加参数传递给ENTRYPOINT。
不好理解是吧?我们举一个例子:
ENTRYPOINT ['/usr/bin/mysql'] CMD ['-h 192.168.100.128', '-p']
假设这个镜像内已经准备好了mysql-client,那么通过这个镜像,不加任何额外参数启动容器,将会给我们一个mysql的控制台,默认连接到192.168.100.128这个主机。然而我们也可以通过指定参数,来连接别的主机。但是不管无论如何,我们都无法启动一个除了mysql客户端以外的程序。因为这个容器的ENTRYPOINT就限定了我们只能在mysql这个客户端内做事情。这下是不是明白了~
因此,我们在使用Dockerfile创建文件的时候,可以创建一个 entrypoint.sh
脚本,作为系统入口。在这个文件里面,我们可以进行一些基础性的自举操作,比如检查环境变量,根据需要初始化数据库等等。下面两个文件是我在SimpleOA项目中添加的Dockerfile和entrypoint.sh,仅供参考:
在准备好Dockerfile之后,我们就可以创建镜像了:
shell docker build -t starlight36/simpleoa .
关于Dockerfile的更详细说明,请参考 https://docs.docker.com/reference/builder/ 。
在产品构建的生命周期里使用Docker, 最佳实践是把Docker集成到现有的构建发布流程里面。 这个过程并不复杂,可以在持续集成系统构建测试完成后,将打包的步骤改为docker build,持续集成服务将会自动将构建相应的Docker镜像。打包完成后,可以由持续集成系统自动将镜像推送到Docker Registry中。生产服务器可以直接Pull最新版本的镜像,更新容器即可很快地实现更新上线。目前Atlassian Bamboo已经支持Docker的构建了。
由于Docker使用联合文件系统,所以并不用担心多次发布的版本会占用更多的磁盘资源,相同的镜像只存储一份。所以 最佳实践是在不同层次上构建Docker镜像。 比如应用服务器依赖于PHP+Nginx环境,那么可以把定制好的这个PHP环境作为一个镜像,应用服务器从这个镜像构建镜像。这样做的好处是,如果PHP环境要升级,更新了这个镜像后,重新构建应用镜像即可完成升级,而不需要每个应用项目分别升级PHP环境。
新手经常会有疑问的是关于Docker打包的粒度,比如MySQL要不要放在镜像中? 最佳实践是根据应用的规模和可预见的扩展性来确定Docker打包的粒度。 例如某小型项目管理系统使用LAMP环境,由于团队规模和使用人数并不会有太大的变化(可预计的团队规模范围是几人到几千人),数据库也不会承受无法承载的记录数(生命周期内可能一个表最多会有数十万条记录),并且客户最关心的是快速部署使用。那么这时候把MySQL作为依赖放在镜像里是一种不错的选择。当然如果你在为一个互联网产品打包,那最好就是把MySQL独立出来,因为MySQL很可能会单独做优化做集群等。
使用公有云构建发布运行Docker也是个不错的选择。 DaoCloud提供了从构建到发布到运行的全生命周期服务。 特别适合像微擎这种微信公众平台、或者中小型企业CRM系统。上线周期更短,比使用IAAS、PAAS的云服务更具有优势。
本文来自 刘思贤 的个人 博客 ,DaoCloud在获得授权后将其转载。
博客原文: Docker技术入门