【编者的话】作者工作于墨西哥IIIEPE研究院,他将通过一系列文章,为我们逐一讲述他们在Docker实际应用过程中的经验与教训,给后来者提供一些参考。本文主要介绍了他基于Docker的开发工作流,包括GitLab、Jenkins、Registry、Nginx。
Docker现在已经两岁了(译者注:Docker于2013年3月13日首次发布), IIIEPE 已经在生产环境中使用Docker3个来月。在此,我分享一些我们的经验和设计的工作流。
我们运行着多个使用Drupal、PHP和Node.js的网站,我们的目标是使用Docker运行所有的应用,因此我们设计了以下工作流:
每一个步骤都需要几天的规划、测试和工作来设计基本准则。
我们做的第一件事是构建满足自己要求的基础镜像。镜像发布到 Docker Hub 中,它们包含了除了应用本身之外所有用于运行应用的东西。每次修改其中一个基础镜像,我们就会运行一个Jenkins任务来拉取新镜像,并触发后续任务来重新构建依赖此基础镜像的所有镜像。
创建完镜像之后,我们需要为所有应用定义一个标准结构。我们所有的应用都使用以下结构来组织:
/application
/logs
/files
Dockerfile
fig.yml.example
docker-compose.yml.example
Makefile
/application 目录是应用的根目录。
/logs 和 /files 目录用于开发,以便应用可以写入日志和文件。这两个目录会被Git忽略,并在生产环境中完全排除。
Dockerfile 是Jenkins用于构建镜像的文件,开发人员几乎不需要接触这个文件,后面详述。
fig.yml.example 和 docker-compose-yml.example 是开发人员用于启动应用的文件。这二者均不用于生产环境,当开发人员克隆一个项目时,他需要复制这个example文件并填入他/她的值。
Makefile 是拼图的最后一块,通过它我们可以拥有一个标准的命令集用于所有应用,并对开发人员隐藏各种各样的复杂性。
每个应用的Dockerfile与其它应用非常类似,这个文件的最重要工作就是构建包含所有待部署代码的最终镜像。我们来看一个例子:
FROM iiiepe/nginx-drupal6
ENV MYSQL_ENV_MYSQL_DATABASE somedb
ENV MYSQL_ENV_MYSQL_USER root
ENV MYSQL_ENV_MYSQL_PASSWORD 123
ENV MYSQL_PORT_3306_TCP_ADDR localhost
ENV MYSQL_PORT_3306_TCP_PORT 3306
ENV BASE_URL http://example.com
ENV DRUPAL_ENVIRONMENT production
EXPOSE 80
RUN usermod -u 1000 www-data
RUN usermod -a -G users www-data
ADD ./application /var/www
RUN chown -R www-data:www-data /var/www
Dockerfile依赖于我们构建的基础镜像,在此之上,它只是设置了一些环境变量默认值、声明要暴露的端口,并将应用代码添加到 /var/www
。
正因为我们构建镜像的这种方式,在Jenkins和开发人员之间唯一的差别是,Jenkins将添加整个应用目录到 /var/www
中,而开发人员只是映射一下目录。
接下来这个是Docker Compose,他非常酷。
mysql:
image: mysql:latest
expose:
- "3306"
ports:
- "3307:3306"
environment:
MYSQL_DATABASE: database
MYSQL_USER: root
MYSQL_PASSWORD: admin123
MYSQL_ROOT_PASSWORD: admin123
web:
image: iiiepe/nginx-drupal6
volumes:
- application:/var/www
- logs:/var/log/supervisor
- files:/var/www/sites/default/files
ports:
- "80:80"
links:
- mysql:mysql
environment:
BASE_URL: http://local.iiiepe.net
DRUPAL_ENVIRONMENT: development
Docker Compose用此文件来初始化。在本例中,我们定义了一个应用,它包括两个容器:一个MySQL容器和一个Web容器。
MySQL容器定义了mysql镜像要使用的环境变量。同时将宿主的3307端口映射到窗口的3306端口上。这允许我们使用任何客户端访问MySQL服务器。
web容器使用了与Jenkins构建最终镜像时使用的相同镜像(见上述Dockerfile),但它同时共享了一些数据卷。在宿主和容器间共享的卷有应用、文件和日志。这实际是开发环境和生产环境间最大的改变:在生产环境中,容器的代码是在镜像中的,这允许我们在任何服务器上启动容器;而在开发环境中,目录只是共享的,因此在应用目录里,任何新的文件或对文件的修改都将即时地反映到容器里。
BASE_URL变量指向了 http://local.iiiepe.net ,这不是个真实的地址,只是用于标准化应用访问的一个方式。因为我们有些人使用Mac和Boot2Docker,我们需要一个标准的地址以便所有人可以将其写入到/etc/hosts文件中。
我的Mac上的/etc/hosts是这样的:
127.0.0.1 localhost
192.168.59.103 local.iiiepe.net
在一台Linux机器上,看起来是这样的:
127.0.0.1 localhost local.iiiepe.net
最后,我们定义了两个环境变量来决定应用的配置文件的一些设置。
Drupal需要一个settings.php来存储数据库信息,包括密码。该文件将被Git忽略,以免将你的密码提交上去,我们决定修改这个文件,让它使用环境变量并将其提交。
以下是一个Drupal 6网站的settings.php里的重要部分:
$username = getenv("MYSQL_ENV_MYSQL_USER");
$password = getenv("MYSQL_ENV_MYSQL_PASSWORD");
$host = getenv("MYSQL_PORT_3306_TCP_ADDR");
$port = getenv("MYSQL_PORT_3306_TCP_PORT");
$database = getenv("MYSQL_ENV_MYSQL_DATABASE");
$db_url = 'mysql://' . $username . ':' . $password . '@' . $host . '/' . $database;
正如你所看到的,没有密码会被提交。密码和其它敏感值将通过ENV变量注入。
有些网站使用Apache Solr作为搜索引擎,但在开发时,我们不希望能写入到Apache Solr,因此需要一个类似DRUPAL_ENVIRONMENT的ENV变量,完成类似下面的settings.php文件的事情:
$conf = array();
if(getenv("DRUPAL_ENVIRONMENT") === "development") {
// Disable apache solr writting
$conf["apachesolr_read_only"] = 1;
}
由于命令很长,使用Docker非常不便,因此Docker Compose(fig)对此很有帮助。我们更进一步尝试让事情变得更简单一些。
这是我们使用在一个Drupal网站上的Makefile:
CURRENT_DIRECTORY := $(shell pwd)
start:
@fig up -d
clean:
@fig rm --force
stop:
@fig stop
status:
@fig ps
cli:
@fig run --rm web bash
log:
@tail -f logs/nginx-error.log
cc:
@fig run --rm web drush cc all
restart:
@fig stop web
@fig start web
@tail -f logs/nginx-error.log
.PHONY: clean start stop status cli log cc restart
使用Makefile比使用Docker Compose或Fig简单得多,因为我们可以创建类似 make cc
的快捷方式来运行类似 drush cc all
这样频繁使用的命令。
有关Makefile的最后一点说明是:我们依然使用Fig。因为在我们设计这个工作流时,Docker Compose还不可用,而且我们团队里的一些开发人员还在使用它,我们决定为Docker Compose建立名为Fig的符号连接,名称更短且更实用。安装Docker Compose后,你可以删除fig并创建符号连接:
sudo rm /usr/local/bin/fig
sudo ln -s /usr/local/bin/docker-compose /usr/local/bin/fig
我们走过一些弯路,我将在别的文章中做介绍,不过有一个我想特别说一下。使用Docker最大的好处是,开发人员可以在与生产环境相同的环境上运行应用,且只损失一点点性能。
我看过一些文章,说他们在Docker之外做开发,然后在需要部署时构建镜像并发送到生产环境。如果你这么做,那你就错了,因为你开发所用的环境与生产机上运行的不一致。不要每次都构建镜像,相反的,在宿主和容器间共享卷,让别人在你每次推送时构建镜像。
未完待续……
文章还远未结束,不过它已经太长了。我们依然需要说明我们是如何整合Maestro-NG、配置Jenkins以及负载均衡器是如何工作的。咱们回见!
原文链接: A production ready Docker workflow (翻译: 梁晓勇 校对:李颖杰)