docker in action advanced

这篇是advanced chapter,主要讨论有:不同项目docker桥接,持久化数据库,docker中多进程。

这篇会假定读者已经对docker有相当程度的了解,懂得docker-compose的写法。

如果正在阅读这篇文章的您并不如何了解docker以及docker-compose,请阅读官方文档以及我的前一篇文章:docker in action

考虑到docker-compose写法有v1和v2两种,特别说明,以下的内容均只保证v2写法。所以请保证您的docker版本高于1.10。

多项目docker桥接

为了更好说明这个议题,我先描述需求。

情景:现在有A,B两个项目,处在不同的文件夹,文件夹为并列关系。A,B均有自己的docker-compose。A是生产者,B是消费者。使用redis作为通信队列。A负责把数据PUSH到这个redis里面,B把数据POP出来,用以做一些操作。

但是A项目写的时间比较早,redis在A项目中是通过link的方式使用的,不暴露端口到外界。那么,B如何才能连接到A的私有的redis?

当然,最简单的方式是,修改B的代码,合并到A中去。在docker-compose中修改引用。这样B可以很简单地通过links连接过去。

小项目当然可以这样。但是我的A,B项目都比较庞大,而且执行的命令和操作不仅差别很大并且都是独立的模块,我并不想合在一起。

另一个办法是改变A中redis的引用方式,改成暴露端口的方式。

但是我也不喜欢这样,我喜欢通过link去使用redis。这样简单而且很安全。

在讨论如何做之前,我们从头理一下,links这个操作是怎么实现的。

很简单,docker-machine会启动一个局域网,同一个项目(假设叫N)中的所有docker容器共用这个局域网,links操作实际上是把被links容器A的局域网中ip写到links容器A的容器B的hosts中。这样就可以ping通了。

那么,实际上,其他的项目M如果期望访问N的内部的A,只要能把M的局域网初始化为N的局域网,就可以访问到A了,不是么?

查看这份文档,会找到一个写法用以引用外部网络。

networks:
  default:
    external:
      name: my-pre-existing-network

ok,网络问题解决。而其中必须的变量name是项目名称。正如文档所述是项目名+_default,不过实践表明,会去掉非字母字符。比如my_project,网络名称就是myproject_default。不过这个其实可以通过docker network inspect看到。

另外,文档没有说的是,M指定了外部的网络,引用外部网络中资源是使用 external_links。而且要指定容器名字。

比如,N中的redis容器,虽然N中是通过links: -redis这样使用,但是如果您使用了 external_links,您需要找到N中redis容器的容器名,通过docker-compose ps去查看。比如我知道了该容器名为compilerweb_redis_1,那么M的docker-compose需要这样写:

external_links:
    - compilerweb_redis_1:redis

持久化数据库

上篇文章讲过,我把数据库也放在docker里面。

其实这很有风险,docker的定位是一个跑无状态服务的容器。正如各位所知,docker容器一旦重启,其内部信息就会逸失,这和数据库的需求是矛盾的。

docker的持久化当然是使用volume,不过,怎样做才是正确的姿势?这是这一节所讨论的内容。

首先一点是,如果您搜索过data persistence docker database相关的内容,能搜索出来很多相关的articles。但是,并不是所有的都是正确的。我试验了很久,某些article中所述的方法,能保证数据持续,但是不一定在docker-compose build, docker-compose up之后不出问题。使用某些article中提供的方式,我在之前掉过一些数据。

不过有一点在articles中均提到过,需要有个docker作为persistence data docker。这是对的。

所以第一步,设置一个data only docker(或者data only container, DOC)。

以postgresql为例 — 我一直使用postgresql,mysql其实是一样的。这个data docker并不干其他事情,只是用于提供一个volume的接口而已。

请在docker-compose中写成这样:

postgresdata:
    image: postgres
    entrypoint: /bin/bash

是的,不需要在data docker中写入任何环境变量,也不需要写入任何表。这个docker只用来提供volume。

您可能注意到了,并没有volumes行,这个难道不是用来提供volume的么?

事实上,postgre官方的dockerfile中已经写了volumes相关的内容。所以,在这里不需要做任何overwrite的事情。直接由image派生出来的docker是最稳定的。因为docker-compose会在检测到变动的时候rebuild docker,这也意味着,容器会被销毁,数据也就丢失了。直接从image派生出来的容器,除非您显式地docker-compose down删掉了容器,否则一定是被pass的。

那么entrypoint: /bin/bash是用来干嘛的?

答案是,复写掉原来postgresql dockerfile中的entrypoint。另外,这个能确保容器退出。

是的,退出。

如果您配置好了所有的项,通过up启动了所有的容器,这时候去看postgresdata的状态,是Exit状态。

有一个误解,docker的容器需要运行,否则无法协作 — 比如,您不可以links到一个exit的容器上去。

但是,有一个例外。volume 容器,如果一个容器提供了volumes,那么,这个容器即使不运行,它的volume也能被其他容器所挂载。关于这一点,在官方文档中有所提及。

于是,这个作为data的容器,只会在初始化的时候被创建,创建完成之后就退出了。之后的docker-compose up命令,由于它来自于image,不可能有更改所以被pass。这也就是说,数据可以说就这样被锁在了这个容器里面。

于是下面是我们真正的数据库容器,数据库的实际操作都在这个容器里面。

postgres:

    build:
        context: .

        dockerfile: MyPostgres

        image: my_postgres:latest

    environment:

        - POSTGRES_USER=test
        - POSTGRES_PASSWORD=test
        - POSTGRES_DB=test
    volumes_from:
        - postgresdata

这个容器只需要挂载来自于postgresdata容器的volume就可以,至于其他的,该怎么弄就怎么弄。

这就是docker database的最佳实践。

另外顺便说说备份的事情。

有一个建议是,再挂载一个容器,这个容器只用来备份。同时这个容器也 volumes_from data容器。然后给容器封装一个pg_dump命令,指向本机的备份文件夹。

不过我有些懒,所以我直接利用的数据库容器。

docker exec -it container_name commend 这个形式可以在该容器中执行命令,结果输出到宿主机。所以,可以 docker exec -it postgre_container pg_dump xxxxxx来执行备份。我把这个命令跑在crontab里面,每天定时备份。

docker中多进程

这并不是说在docker中使用多进程多线程这个意义。

我不太清楚有一个细小的地方您是否知晓。

docker中,只能跑一个服务。

这也就是说,假设您有一堆脚本,A,B,C,D。然后您打算创建一个 docker 容器,用来运行所有的脚本。

可能您期望在这个容器的dockerfile里面这样写:

CMD ["python", "A.py"]
CMD ["python", "B.py"]
CMD ["python", "C.py"]
CMD ["python", "D.py"]

但是您会发现,最终在这个容器中跑的脚本,只有D。

如果一个脚本就一个容器,这样实在太傻。

更好的方式是利用supervisor。supervisor是一个服务,或者说进程。通过在supervisor的配置文件中指定命令,supervisor可以执行该命令。并且,根据配置,您可以确保,一旦该命令停止(或者说脚本异常退出),supervisor会自动启动。

我这里给一个supervisor的基础dockerfile。

FROM debian:latest

RUN \
        sed -i 's/# \(.*multiverse$\)/\1/g' /etc/apt/sources.list && \

        apt-get update && \

        apt-get -y upgrade
RUN apt-get update && apt-get install -y openssh-server apache2 supervisor
RUN mkdir -p /var/lock/apache2 /var/run/apache2 /var/run/sshd /var/log/supervisor

强烈推荐把这个压成一个镜像之后,再在这个镜像的基础上派生容器。

比如,您把这个镜像命名成supervisor,然后再写一份dockerfile:

FROM suptervisor

your commend

COPY supervisor.conf /web/supervisor.conf
# default command
CMD ["supervisord", "-c", "/web/supervisor.conf"]

上面提到的supervisor.conf是标准的supervisor配置,怎么写有很多资料,不累述。

之所以这么干是因为docker-compose的缓存机制不会缓存有apt-get update相关的命令的dockerfile。所以,如果不把第一份配置压成镜像,那么每次docker-compose up都会执行一次。apt-get install是很慢的。所以压成镜像之后,这部分的过程和等待时间就可以省去了。

当然,如果您是在写公司代码,那么大可以把两份配置合并。毕竟,等待下载和编译也是工作的一环嘛,或许还可以趁机吃一顿晚餐?

以上就是advanced的内容。

我认为这些应该是会在实践中很容易遇到,但是网络上资料凌乱并且不成体系的部分。

这姑且是我的最佳实践。如果对您有帮助就再好不过。

希望您喜欢。

谢谢。

AS,落笔于闷热的六月午后。