这篇是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,落笔于闷热的六月午后。