Docker实践(9) – 虚拟机转换为容器

Docker Hub没有所有可能的基本镜像,所以对于一些小众的Linux发行版本和用例,人们需要自己来创建它。如果你想把一个存在状态的虚拟机放入Docker上层迭代,或者受益于Docker生态系统,同样的原则也适用。
理想情况下,你希望使用标准的Docker技术从头开始构建一个等效的VM,例如Dockerfiles与标准配置管理工具。然而现实是,许多VM没有仔细配置管理。这个有可能发生,因为一个VM已经发展成为一个有机体,人们已经在用它了,以一个更结构化的方式来重新创建它的投资可能不值得。

问题

你想把一个VM转换为一个Docker镜像

解决办法

通过ssh使用qemu-nbd,tar或者其它方法把VM文件系统打包成一个TAR文件,然后在Dockerfile中使用ADD命令添加TAR文件来创建镜像。

讨论

首先我们要将虚拟机分为两大类:本地(VM磁盘镜像和VM的执行在你的电脑)和远程(VM磁盘镜像的存储和VM执行在其它地方)。两组虚拟机(和任何你想创建Docker镜像)转换为Docker镜像的原理是一样的 – 打包文件系统为TAR文件并添加TAR文件到scratch镜像的根目录。
ADD命令 – ADD Dockerfile命令(不像COPY命令)当把TAR文件(也可以是gzipped文件和其它类似的文件类型)放置到镜像时会自动解压。
SCRATCH镜像 – scratch镜像是你在上面创建镜像的一个零字节的假镜像,通常它用于你想使用Dockerfile复制(或添加)一个完整文件系统到一个镜像的情况。
我们现在来看一个你有一个本地Virtualbox VM的情况。
在我们开始之前,你需要做如下事件:

  • 安装qemu-nbd工具(在Ubuntu系统的qemu-utils包里)
  • 记下VM磁盘映像的路径
  • 关闭VM
  • 如果你的VM磁盘映像是.vdi或.vmdk格式,应该能使用qemu-nbd成功挂载。其它的格式也有可能可行。
    以下代码演示了如何将虚拟机文件转换为虚拟磁盘,然后你可以从中复制所有文件:
    虚拟化技术
    如果你的VM是在远程,你可以请求你的运维团队来对你想要的分区作一个转储,或者在VM运行时创建TAR。
    如果你得到一个分区转储,可以很容易的挂载并打包成一个TAR文件:

    1. $ sudo mount -o loop partition.dump /mnt
    2. $ sudo tar cf $(pwd)/img.tar -C /mnt .
    3. $ sudo umount /mnt

    或者你可能在正在运行的系统上创建TAR文件:

    1. $ cd /
    2. $ sudo tar cf /img.tar –exclude=/img.tar –one-file-system /

    你现在得到了包含文件系统的TAR文件,可以通过scp命令传输到其它机器。

    从正在运行的系统上创建TAR文件看起来是一个最简单的方法(不用关机,安装软件或者请求其他团队),不过它有一个严重的缺点 – 你可能会一个不一致的状态下复制文件,这样当使用Docker镜像时会遇到奇怪的问题。如果你必须这样做,那么就尽可能的关闭应用和服务。

    一旦你得到了文件系统TAR文件,你就可以添加到镜像了。这是最简单的一步,只有Dockerfile的两行:

    1. FROM scratch
    2. ADD img.tar /

    然后执行docker build .就生成镜像了。
    现在你得到了一个新的镜像,你可以运行一个容器,然后在上面做你想要的修改,比如删除不需要的包来缩减容器大小并重新导出为一个新的小的镜像,下面是流程图:
    虚拟化技术

    Docker实践(8) – 搜索和运行一个Docker镜像

    Docker registries启用了类似于GitHub的社交编程文化。如果你有兴趣尝试一个新的软件应用,或者搜索一个新的软件用于特定目的。那么Docker镜像可以是一个简单的试验方式,而不会干扰你的主机,调配VM或者担心安装步骤。

    问题

    你想找到以Docker镜像形式的应用或工具,并尝试使用它。

    方法

    使用docker search命令来找到镜像来拉取并运行它。

    讨论

    我们假设你有兴趣尝试Node.js。在下面的命令中我们使用docker search命令来搜索带有node字符的镜像:
    虚拟化技术
    虚拟化技术
    一旦你选择好镜像,你可以通过执行docker pull imagename命令来下载它。
    虚拟化技术
    然后你可以使用-t和-i参数以交互模式运行它。-t参数创建一个tty设备(一个终端),-i参数指定Docker会话为可交互的。

    1. $ docker run -t -i node /bin/bash
    2.         root@c267ae999646:/# node
    3.         > process.version
    4.         ‘v0.12.0’
    5. >

    从镜像维护者经常会有关于如何运行镜像的特别建议。在http://hub.docker.com网站搜索这个镜像会把你带到这个镜像的页面。Description tab会给你更多的信息。

    Docker实践(7) – 设置本地私有库registry

    你看到了Docker官方有一个服务,人们可以公开分享他们的镜像(你可以付费设置为私有)。不过有些理由使你不想通过Hub分享镜像 – 一些企业喜欢把服务尽可能的留在内部,或者可能是你的镜像太大了,通过互联网传输会太慢,或者可能你只是在做试验想暂时保持私有,或者不想付费购买私有权限。不管是什么样的原因,有一个简单的解决方案。

    问题

    你想要一个本地托管你的镜像的方法。

    方法

    在你本地网络设置一个registry服务器。

    讨论

    要运行registry,在一个空间充足的机器执行如下命令:

    1. $ docker run -d -p 5000:5000 -v $HOME/registry:/var/lib/registry registry:2

    此命令设置了docker主机的5000端口作为registry的服务端口并设置容器的/var/lib/registry为registry的存储目录,也就是registry在容器中存储文件的默认目录。
    在需要访问registry服务器的docker机器上,给守护端添加如下选项(HOSTNAME是registry服务器的主机名或IP地址):–insecure-registry HOSTNAME.
    你现在能执行docker push HOSTNAME:5000/image:tag命令把镜像推送到registry服务器。
    如你所见,配置一个镜像存储在$HOME/registry的本地registry只用到了最基本的配置级别,很简单吧。如果你想扩大registry的存储规模和使服务更健壮,可以查看支持的存储驱动(https://github.com/docker/distribution/blob/v2.2.1/docs/storagedrivers.md),如存储到Amazon S3。
    你可能想了解关于–insecure-registry的选项。为什么帮助用户保持服务安全性,Docker只允许你从具有签名HTTPS证书的registrys拉取镜像。我们关闭此安全选项是因为我们认为本地网络是安全的。

    Docker实践(6) – 容器内部连接(不需要在主机上映射端口)

    上一个技术点展示了如何通过暴露端口来开放你的容器到主机网络。不过当你只是想容器内部之间连接时,就不再想把端口暴露给主机网络或外部网络了。
    在本文将介绍如何通过Docker link参数来实现这个目的,来确保外部无法访问你的内部服务。

    问题

    你想要允许容器内部之间的连接。

    方法

    通过docker的链接功能来允许容器相互通信。

    讨论

    继续我们wordpress的设置,我们打算从wordpress容器分离出mysql数据库,然后不通过端口映射方式来使它们能够相互通信。如图:
    虚拟化技术
    按照如下命令来运行容器,第一个和第二个命令相隔一分钟执行:

    1. $ docker run –name wp-mysql -e MYSQL_ROOT_PASSWORD=yoursecretpassword -d mysql
    2. $ docker run –name wordpress –link wp-mysql:mysql -p 10003:80 -d  wordpress

    在第一个命令中,给容器设置了wp-mysql名称以便之后引用。并且提供一个环境变量(-e MYSQL_ROOT_PASSWORD=yoursecretpassword)来使mysql容器能够完成初始化。
    在第二个命令中,以wordpress命名容器,并通过–link wp-mysql:mysql来连接wp-mysql与wordpress容器。注意链接不会等待链接容器启动完成,所以上一步的操作需要等待一分钟以便让mysql容器先完成启动。不过最好的方法是通过docker logs wp-mysql来查看mysql容器是否已经准备接受连接了。
    在这个示例中最重要的是–link参数,这个参数使wordpress容器自动维护/etc/hosts文件来使wp-mysql mysql等名称指向mysql容器IP。

    启动次序事项:容器必须按正确的次序来启动以便存在的容器映射能够正确的替换。目前为止链接的动态解析不是docker的一项功能。

    为了能够让容器能够被连接,比如mysql容器,就需要在创建mysql镜像时暴露端口,如暴露3306端口,这个可以在Dockerfile使EXPOSE完成。
    至于为什么为这样做,请参考http://dockone.io/article/455

    Docker实践(5) – 通过端口连接容器

    Docker容器从一开始就设计用来运行服务。在大多数情况下,会是一种HTTP服务或其它。其中很大一部分是通过浏览器访问的Web服务。
    这会导致一个问题。如果你有多个运行在它们内部环境的80端口上的Docker容器,它们不能都通过你机器上的80端口访问。下一次技术点将介绍如何通过从你的容器暴露和映射一个端口来处理这个常见的情景。

    问题

    你想通过你机器上的端口使你的容器能够得到访问。

    方法

    使用docker的-p参数来映射容器的端口到你的主机。

    讨论

    在这个示例中我们准备用tutum-wordpress镜像。假设你要在主机上运行两个容器来创建不同的博客。因为很多人想要这样做,有人已经准备好一个镜像让其他人能够获取和启动。你可以使用docker pull命令来得到外部位置的镜像。默认下,镜像会从docker hub去下载。

    1. $ docker pull tutum/wordpress

    运行第一个博客,执行如下命令:

    1. $ docker run -d -p 10001:80 –name blog1 tutum/wordpress

    docker run命令以守护进程(-d),参数-p运行容器。它标识要映射主机的10001端口到容器的80端口,并设置了一个容器名称用来识别它(–nameblog1tutum/wordpress)。
    你可以用同样的方法创建第二个博客:

    1. $ docker run -d -p 10002:80 –name blog2 tutum/wordpress

    如果你现在运行这个命令,

    1. $ docker ps -a | grep blog

    你会看到两个博客窗口列出,有它们的端口映射,类似如下:
    9afb95ad3617 tutum/wordpress:latest “/run.sh”
    ➥ 9 seconds ago Up 9 seconds
    3306/tcp, 0.0.0.0:10001->80/tcp blog1 31ddc8a7a2fd tutum/wordpress:latest
    ➥ “/run.sh” 17 seconds ago Up 16 seconds 3306/tcp, 0.0.0.0:10002->80/tcp blog2
    你可以在浏览器中通过http://localhost:10001和http://localhost:10002访问这两个博客容器。
    完成后要删除这些容器(假设你不想保留):

    1. $ docker rm -f blog1 blog2

    如果需要,你现在应该能够通过自己管理端口映射来运行多个相同的镜像来提供不同的服务了。

    Docker实践(4) – 使用socat查看Docker API流量详情

    docker命令可能会偶尔地无法正常工作。大多数情况下是命令行参数没有输入正确,不过也有偶尔一些是严重的配置问题,如Docker二进制文件过期了。为了诊断此问题,查看你正在连接的docker daemon的数据流可能会有所帮助。

    问题

    你想用docker命令来调度一个问题。

    方法

    使用流量探测器来检查API调用并输出调用详情。

    讨论

    在此技术中你将在你的请求与服务器socket之间安放一个Unix域套接字代理来查看传过来了什么东西。现在需要root或sudo权限来做这件事。
    要创建这个代理,需要用到socat命令。

    socat是一个允许你在几乎任何类型的两个数据通道之间中继数据。

    1. $ sudo socat -v UNIX-LISTEN:/tmp/dockerapi.sock
    2.           UNIX-CONNECT:/var/run/docker.sock &

    虚拟化技术
    在这个命令中-v参数输出数据流的可读格式。UNIX-LISTEN让socat监听一个域套接字,UNIX-CONNECT让socat连接到Docker的域套接字。’&’使命令后台运行。
    你的请求到docker的路由可以看下面。
    所有的流量都将被socat看到并记录到你的终端。
    虚拟化技术
    一个简单的docker命令输出类似如下:
    虚拟化技术
    socat不仅仅是用来调试docker,也可以用来调试任何的网络服务。

    Docker实践(3) – 移动docker到一个不同的分区

    Docker把与你的容器和镜像相关的数据保存到一个目录下。由于可能会存储潜在的大量不同的镜像,这个目录会很快变得很大!
    如果你的主机有不同的分区,你可能会更快遇到空间受限的问题。在这种情况下,你可能需要把Docker的数据目录移动到其它分区。

    问题

    你想要移动docker的数据

    方法

    停止docker daemon,使用-g参数指定新目录启动。

    讨论

    首先你需要停止你的docker daemon。
    假设你想把docker数据放在/home/dockerusr/mydocker目录下,我们先把原来的目录迁移过来:

    1. cp -a /var/lib/docker/* home/dockerusr/mydocker/

    然后启动docker:

    1. docker daemon -g /home/dockeruser/mydocker

    这时候你通过执行docker images或docker ps -a就可以看到原来的镜像或容器。确认好之后就可以删除原来的数据了。
    当然记得把此更改添加到配置文件。

    Docker实践(2) – 以守护进程运行容器

    当你逐渐了解docker后,你会开始考虑docker的其它用例,第一个首先是运行服务一样运行docker。

    问题

    你想作为一个服务在后台运行docker.

    方法

    docker run命令使用-d参数,和使用与容器管理相关的参数来定义服务特性。

    讨论

    Docker容器 – 像大多数进程 – 默认在前台运行。在后台运行docker容器最常见的方式是使用&控制操作符。即使这个可行,不过当你的日志输出到终端会话时可能会遇到麻烦,或者你使用nohup命令,它会当前目录创建一个日志输出文件,不过增加了一个日志管理的问题。而docker提供有在后台运行的功能,如:

    1. $ docker run -d -i -p 1234:1234 –name daemon ubuntu nc -l 1234

    当docker run命令使用-d参数时,docker会以守护进程运行。-i参数使你能够与Telnet会话交互。-p参数使容器的1234端口绑定到主机。–name参数设置容器一个名称方便以后引用。最后,运行了一个监听在1234端口的echo服务器。
    如果你现在连接并使用telnet发送消息,你使用docker logs命令会看到容器已经接收到消息,如图:
    虚拟化技术
    你看到以守护进程运行容器足够简单,但操作上仍有一些问题需要回答:
    * 服务失败时会怎样?
    * 服务终止时会怎样?
    * 如果服务持续故障会怎样?
    幸运地是docker为每个问题提供了参数!

    虽然restart参数经常与-d参数一起使用,但技术上来说这不是必要的。

    docker run –restart命令允许容器终止时应用一系列的规则(也称重启策略)。
    重启策略可选值:
    no – 容器退出时不重启
    always – 容器退出时始终重启
    on-failure[:max-retry] – 只在发生错误时重启
    no策略很简单:当容器退出时,不重启容器。这个是默认值。
    always也同样简单,不过值得简单介绍下:

    1. $ docker run -d –restart=always ubuntu echo done

    此命令以守护进程运行容器,并在进程终止时始终重启容器(–restart=always)。执行了一个快速完成的echo命令,然后退出容器。
    如果你执行以上命令然后执行docker ps命令,会看到类似的输出:
    虚拟化技术
    注意STATUS列告诉我们容器少于一秒前退出,正在重启。这是因为echo done命令立即退出了,docker就必须持续地重启容器。
    最后on-failure策略只在容器以非0(非正常失败)状态码退出时重启:

    1. $ docker run -d –restart=on-failure:10 ubuntu /bin/false

    此命令以守护进程运行(-d)和对尝试重启设置一个限制值(–restart=on-failure:10),超过此次数时不再重启。它运行一个简单的命令(/bin/false),会快速完成且肯定会失败。
    如果你执行以上命令,等一分钟后执行docker ps -a,你会看到类似的输出:
    虚拟化技术

    Docker实践(1) – 设置守护进程对外开放

    虽然默认下你的Docker daemon只允许在本机访问,但有时候也有充足理由让其他人访问。你可能有什么问题想让其他人远程调试,或者你可能想允许DevOps工作流来启动主机上的一个进程。

    INSECURE!虽然这是一个强大且有用的技术,但是这认为是不安全的。一个开放的Docker daemon可能被其他人利用来获取系统权限。

    问题

    你想对其他人开放Docker服务器的访问权限

    方法

    以开放的TCP地址启动Docker daemon

    讨论

    以下图是这种技术的工作原理.
    虚拟化技术
    在你开放Docker daemon之前,首先得关闭它。如何关闭它取决于你的系统,如果你不清楚,先执行如下命令:

    1. $ sudo service docker stop

    如果你得到如下信息:
    The service command supports only basic LSB actions (start, stop, restart,
    try-restart, reload, force-reload, status). For other actions, please try
    to use systemctl.
    表示你的系统启动管理是基于systemctl的,尝试如下命令:

    1. $ systemctl stop docker

    如果这个可行,那么下面的命令将没有任务输出:

    1. ps -ef | grep -E ‘docker (-d|daemon)b’ | grep -v grep

    一旦Docker daemon已经停止,你就可以使用以下的命令手动重启来打开对外用户的访问:

    1. docker daemon -H tcp://0.0.0.0:2375

    此命令以守护进程启动docker,使用-H参数为定义主机服务器,使用TCP协议,监听所有IP(使用0.0.0.0),开放docker服务器标准端口2375。如果Docker提示daemon是一个无效子命令,尝试使用旧的-d参数。
    之后你可以在外面使用如下命令来访问docker:

    1. $ docker -H tcp://<your host’s ip>:2375

    注意你在本机也使用用此方法来连接docker,因为docker已经不再监听默认的地址。
    如果你想让此更改永久生效(即使重启后),虽然更改docker的配置文件:
    系统Ubuntu / Debian / Gentoo配置文件为:/etc/default/docker
    OpenSuse / CentOS / Red Hat配置文件为:/etc/sysconfg/docker
    在配置文件中找到DOCKER_OPTS,设置为以上的启动参数,如DOCKER_OPTS=”daemon -H tcp://0.0.0.0:2375″
    如果你的系统使用的是systemd配置文件,那么在/usr/lib/systemd/system/service/docker中找到ExecStart行,设置为ExecStart=/usr/bin/docker daemon -H tcp://0.0.0.0:2375

    开放docker daemon后,建议使用防火墙来限制对指定IP开放访问,而且不使用0.0.0.0,非常不安全。

    docker化你的PHP应用环境Nginx PHP-FPM

    开始之前

    在开始之前,我们假设你对Docker使用已经有了一些经验。本文尝试使用Docker官方存储库来运行一个简单的PHP应用程序,环境是Nginx和PHP。首先第一件事当然是安装Docker(如果你还没安装)。第二获取Docker Compose。

    配置Nginx

    我们从配置Web服务器开始,这里我们使用Docker官方的Nginx镜像。既然我们准备使用Docker Compose,我们接下来先创建docker-compose.yml文件,其实docker nginx的80端口映射到宿主机的8080端口:

    1. web:
    2.  image: nginx:latest
    3.  ports:
    4.  – "8080:80"

    现在我们运行:

    1. docker-compose up

    现在我们应该通过ip:8080会得到nginx的默认页面了。
    Nginx
    现在nginx是启动了,让我们再来添加一些配置。首先更新docker-compose.yml使它挂载到一个本地目录。我们使用目录code。

    1. web:
    2.     image: nginx:latest
    3.     ports:
    4.         – "8080:80"
    5.     volumes:
    6.         – ./code:/code

    下一步我们创建一个虚拟机配置文件site.conf,设置其根目录为/code,把该文件放置到与docker-compose.yml同级目录。

    1. server {
    2.     index index.html;
    3.     server_name php-docker.local;
    4.     error_log  /var/log/nginx/error.log;
    5.     access_log /var/log/nginx/access.log;
    6.     root /code;
    7. }

    为了让这个虚拟机配置生效,我们还需要再次更改docker-compose.yml文件:

    1. web:
    2.     image: nginx:latest
    3.     ports:
    4.         – "8080:80"
    5.     volumes:
    6.         – ./code:/code
    7.         – ./site.conf:/etc/nginx/conf.d/site.conf

    此次更新是把本地的site.conf挂载docker nginx里的/etc/nginx/conf.d/site.conf,这样这个配置文件就被添加到docker nginx里了。下面我们执行这个命令生效它:

    1. docker-compose up

    配置PHP

    现在我们已经把nginx配置好了,接下来配置PHP。首先是拉取官方的PHP7-FPM镜像,并链接到Nginx容器,docker-compose.yml如下:

    1. web:
    2.     image: nginx:latest
    3.     ports:
    4.         – "8080:80"
    5.     volumes:
    6.         – ./code:/code
    7.         – ./site.conf:/etc/nginx/conf.d/site.conf
    8.     links:
    9.         – php
    10. php:
    11.     image: php:7-fpm

    下一步是配置nginx使用php-fpm容器来解析php文件。更新后的site.conf如下:

    1. server {
    2.     index index.php index.html;
    3.     server_name php-docker.local;
    4.     error_log  /var/log/nginx/error.log;
    5.     access_log /var/log/nginx/access.log;
    6.     root /code;
    7.  
    8.     location ~ .php$ {
    9.         try_files $uri =404;
    10.         fastcgi_split_path_info ^(.+.php)(/.+)$;
    11.         fastcgi_pass php:9000;
    12.         fastcgi_index index.php;
    13.         include fastcgi_params;
    14.         fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
    15.         fastcgi_param PATH_INFO $fastcgi_path_info;
    16.     }
    17. }

    为了测试,让我们在./code目录新建一个index.php,内容如下:

    1. <?php
    2. phpinfo();
    3. ?>

    然后通过ip:8080打开,但出现File not found错误。这是因为PHP容器找不到此文件,我们也需要像nginx一样挂载code目录,更新docker-compose.yml文件如下:

    1. web:
    2.     image: nginx:latest
    3.     ports:
    4.         – "8080:80"
    5.     volumes:
    6.         – ./code:/code
    7.         – ./site.conf:/etc/nginx/conf.d/site.conf
    8.     links:
    9.         – php
    10. php:
    11.     image: php:7-fpm
    12.     volumes:
    13.         – ./code:/code

    最后,启动容器:

    1. docker-compose up

    我们将会看到正常的phpinfo页面
    Nginx