上一文中我们完成了基础环境的安装。
本文将要学习 Docker Image 的自定义,及 使用Docker Compose进行环境部署的方法。
文章索引
- GitLab、Jenkins、Docker 初始环境安装
- 制作 Docker镜像 及 Docker Compose 的使用(本文)
- 使用 Webhook 自动触发 Jenkins 进行 Docker镜像制作并保存到私有镜像仓库,以及目的机部署
提示:本文是基于 Docker 1.11 编写的,从 2016 年 4月 至今,Docker 已经改变了许多。基本用法没有改变,只不过增加了很多新特性,并移除了部分不再使用的标签。至2018年为止,本文依旧适用于入门学习,但部分新特性的改变(比如 aufs->overlays ),以及部分标签的移除(比如 MAINTAINER->LABEL ),请参照 Docker 官方文档为准。
Docker一些概念
国内各个厂家对Docker概念都翻译不一样,我这里先写一下我自己的概念理解
- 镜像(image):镜像
- 容器(container):由单一服务构成的针对最基本功能实现的 服务(service)(比如Nginx)
- 项目(project):多个服务构成的 应用(application)(比如由 Nginx + PHP + Mysql + WordPress组成的博客)
单个容器只能提供非常基本的服务,比如单纯的Mysql数据库服务,单纯的Redis缓存服务。
需要由多个容器协同工作,才能组成一个复杂且强大的项目,实现产品功能。
这里鄙视以下各个厂家为圈地,拉拢用户,使用了非常多的混淆概念,比如引入微服务(microservice)。
什么是他妈的微服务,微博微信朋友圈玩多了吧。不论按照Windows还是按照Linux来讲,服务(service)指的都是由单一应用程序完成的最基本的功能实现。还他妈的微,是不是还要把HTTP服务拆成 HyperText Transfer Protocol 之后分三段去实现啊。
维基百科上有针对微服务的定义:微服务 (Microservices) 是一种软件架构风格 (Software Architecture Style),它是以专注于单一责任与功能的小型功能区块 (Small Building Blocks) 为基础,利用模组化的方式组合出复杂的大型应用程序,各功能区块使用与语言无关 (Language-Independent/Language agnostic) 的 API 集相互通讯。微服务是由以单一应用程序构成的小服务,自己拥有自己的行程与轻量化处理,服务依业务功能设计,以全自动的方式部署,与其他服务使用 HTTP API 通讯。同时服务会使用最小的规模的集中管理 (例如 Docker) 能力,服务可以用不同的编程语言与数据库等元件实作。
很明显这里的微服务指的与我们所说的服务是相同的,只不过更强调其单一性,即甚至要把Apache+PHP这种组合也剥离开(传统模式下,PHP是以模块形式与Apache共同工作的,而Nginx与PHP是靠PHP-FPM共同工作,后者更颗粒化)
然而国内不少网站把微服务当做微信、微博一个级别的概念对待,以为微服务就是产品功能级的服务,订单微服务,通知微服务,狗屎微服务。
使用Docker实现一个项目
我们在这里,要基于nginx、php-fpm,实现一个PHP应用。这个项目将作为一个基础学习项目,对接下来的Jenkins和最终部署都有很大关系。
将要做的事
首先先给大家看一下我的目录结构,如果你对我写的整个结构都很一目了然的话,证明你对这一部分都很熟悉,你可以跳过这一章节了。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | [root@localhost docker]# tree . ├── docker-compose.yml ├── getInDocker.sh ├── jenkins_modified │ ├── Dockerfile │ └── etc │ ├── localtime │ └── timezone ├── nginx_modified │ ├── conf │ │ ├── conf.d │ │ │ └── default.conf │ │ ├── fastcgi.conf │ │ ├── mime.types │ │ └── nginx.conf │ └── Dockerfile ├── php-fpm_modified │ ├── conf │ │ └── www.conf │ └── Dockerfile ├── project-build.sh └── www-data ├── Dockerfile └── web ├── a.txt └── index.php |
我们需要准备nginx和php-fpm的Dockerfile,从Docker Hub上获取。编写一个静态文件和一个php文件,并为自己的代码制作一个Dockerfile用于生成数据卷容器。
没人说过容器里不能放纯代码的。纯代码,满足最基本颗粒化要求(存储),无状态或状态不重要(随用随建,不用即删,临时文件不重要)。
至于为什么要专门为php等代码建立数据卷容器,而不是直接使用git或其他代码管理工具同步到硬盘上的原因,将在后面介绍。
nginx
Let’s make a nginx_modified
确认配置方法
首先我们要知道nginx的镜像内部到底是怎样的。有两种方法:
第一种,生成一个默认容器,并进入容器内部观察结构。这是一种不推荐的做法,因为除非你对整个服务有及其充分的了解,否则你永远摸不清它里面到底是怎样的。这就相当于你作为一个人类活了一辈子,除非你是专业的医生,否则你是没办法拿手术刀给别人做手术的。
第二种,根据Docker Hub上的描述来修改相应的配置。这是推荐的办法,多数项目都会对自己的docker项目有比较详细的描述。而且多数项目可能会针对Docker做一些调整,他们的配置与传统的编译安装配置也是不同的。使用推荐的办法修改配置是最安全的办法。
除非他们自身的描述写的相当渣,或者完全就没有描述,否则不要使用第一种方法。
Dockerfile
Dockerfile是用于构建镜像的文件,其内包含了多指令,每一条指令构建一层。
这是为自定义nginx服务而编写的dockerfile
1 2 3 4 5 6 7 8 9 10 11 | FROM nginx:1.10.2
MAINTAINER catscarlet
ENV NGINX_VERSION 1.10.2-1~jessie
COPY conf/ /etc/nginx
EXPOSE 80 443
CMD ["nginx", "-g", "daemon off;"] |
逐条解释下:
FROM:基于哪个版本的镜像生成新的镜像,我们这里选择stable版的1.10.2。如果这里不指定版本号,docker会默认使用latest版本(2016年11月24日为1.11.5)
MAINTAINER:这个镜像的作者名
ENV:指定环境变量,这里声明了nginx的版本
COPY:将本地conf/中的文件复制到镜像的/etc/nginx目录下。使用这个办法我们替换了nginx的配置文件。包括默认网站、对php文件的处理等。
EXPOSE:可映射的端口
CMD:容器启动时要执行的命令
VOLUME:定义匿名卷
nginx的default.conf
1 2 3 4 5 6 7 8 9 10 11 12 | server { listen 80; root /var/www/html; index index.php index.html;
charset utf-8;
location ~ \.php$ { fastcgi_pass php-fpm:9000; include fastcgi.conf; } } |
生成镜像:
1 | docker build -t 'nginx_app' nginx_modified/ |
测试启动:
1 | # docker run -d -p 10080:80 nginx_app |
因为我们的设备上还跑着GitLab和Jenkins呢,所以要避开许多端口,这里选择使用10080。
使用浏览器打开本机的10080端口,这个时候/var/www/html并没有映射到任何数据卷,所以看到的应该是nginx的默认欢迎界面。
底层执行docker ps
查看这个容器,使用docker down {container_id}
停止这个容器,使用docker rm {container_id}
删除这个容器。你也可以在容器未停止时,直接使用docker rm -f {container_id}
强制删除这个容器。
提示:你可以使用docker rm $(docker ps -a -q)
删除所有已停止的容器
底层执行docker image
查看生成的镜像信息,使用docker rmi {image_id}
删除这个镜像。
提示:你可以使用docker rmi $(docker images -q -f "dangling=true")
删除所有无tag的镜像。
PHP-FPM
接下来我们制作PHP-FPM的镜像。
其实完全没有必要,PHP-FPM不仅是真正的无状态的,而且几乎没有修改配置的必要。
Dockerfile,我们只指定以下版本就好了
1 2 3 | FROM php:7.0.13-fpm
MAINTAINER catscarlet |
默认情况下,会继承原镜像的端口映射9000。
代码镜像
这里再次强调一下,Docker的镜像是分层文件系统,我们的自定义nginx镜像并不会占用一个整个nginx镜像所需的硬盘空间,只占用我们添加的配置文件的空间大小,而所需的nginx则继续使用FROM中的镜像。
那么对于没有基础依赖的代码镜像,应该FROM什么镜像呢?
网上有不少办法是基于一个最小化的Linux操作系统来做,比如Busybox。Bullshit!
注意一下现在所有的Docker镜像,生成他们的Dockerfile都是有FROM的,而且一般都是 debian:jessie。这意味着多数镜像都是依赖debian:jessie的,debian:jessie已经存在于我们的本地镜像库中。我们也可以继续基于debian:jessie制作镜像。
Dockerfile
1 2 3 4 5 6 7 | FROM debian:jessie
MAINTAINER catscarlet
COPY web/ /var/www/html/
VOLUME '/var/www/html/' |
web/a.txt
1 | This is a text file. |
web/index.php
1 2 3 4 5 6 7 8 9 10 | <?php
ini_set('date.timezone', 'Asia/Shanghai'); header('Content-Type:text/plain; charset=utf-8');
$hostname = file_get_contents('/etc/hostname'); $date = date('r', time()); $rst = 'This is Server: '.$hostname."\n".'Now is '.$date;
echo $rst; |
Docker Compose
Docker的每个容器只完成最基本的服务,而想要实现一个产品功能,往往需要多个基本服务共同完成。对于每个服务来讲他们是无依赖的,但对于项目来讲,他们就是需要紧密结合的。
Docker Compose就提供了这样一个功能,允许定义一组容器,组成一个项目。
安装
执行命令如:
1 2 | curl -L https://github.com/docker/compose/releases/download/1.9.0/docker-compose-`uname -s`-`uname -m` > /usr/local/bin/docker-compose chmod +x /usr/local/bin/docker-compose |
官方也提供pip安装方式,但依赖和局限性比较多,在不同的系统上有不同的问题,不建议用这种方式安装
相关文档:https://docs.docker.com/compose/
配置
Docker Compose使用YAML格式的配置文件:docker-compose.yml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 | www-data: build: ./www-data
php-fpm: build: ./php-fpm_modified expose: - 9000 volumes_from: - www-data
nginx_app: build: ./nginx_modified expose: - 80 links: - php-fpm volumes_from: - www-data ports: - "9080:80" |
主要使用方法:直接在docker-compose.yml的路径下,执行以下命令:
- docker-compose build 新建(或重建)项目中所有需要build的镜像。此步非必须。
- docker-compose up 新建(或重建)项目中所有需要build的镜像,并启动对应项目。这是一个前台操作,在所有容器内所有进程退出之前不会退出,使用Ctrl-C会停止容器,但不会删除容器。使用 -d 参数使项目在后台启动。
- docker-compose start 启动一个已经存在的项目。
- docker-compose stop 停止一个已经运行的项目。
- docker-compose down 停止一个已经运行的项目,并删除所有相关联容器。
启动项目
在docker-compose.yml的路径下执行docker-compose up -d。
现在访问你的服务器http://your-server:9080/a.txt
应该可以打开这个静态文件 a.txt,http://your-server:9080/index.php
可以打开执行后的php结果了。
一些问题
代码位置问题
有很多开发者会把要运行的代码与服务打包到同一个镜像中。
这些开发者一般多为JAVA开发者。对于JAVA开发者来讲,他们的项目结构可能是这样的:TOMCAT + JAVA包 + 数据存储。对于 TOMCAT + JAVA包 这种情况便是强依赖环境,同样符合无状态和最小颗粒的特性,或许并没有问题。
然而对于其他语言的开发者,项目结构一般为 HTTP服务 + 语言解析器 + 代码 + 数据存储。
以PHP开发者举例,可以理解为 NGINX + PHP-FPM + 代码 + Mysql。而代码中不仅包含php代码,也会包含html、js、css等代码。
将代码单独的放入 NGINX 镜像或者 PHP-FPM 镜像,都会导致运行过程中,容器之间无法获取代码,解析时出现404错误。
另外在日常开发时,经常变动的也只有代码部分,其他镜像一般多为万年不变的。将代码独立到单独容器中也会减小部署压力。
(目前还没见到过PHP开发者使用Docker和做持续化部署的)
配置文件问题
我们对标准的nginx镜像进行了修改,添加和修改了配置文件。对于应可以随时替换的标准服务来讲这并不完美。于是有一种思路,就是把配置文件像代码一样也做成数据卷容器。这样做未尝不可,但难度会很大。因为挂在数据卷的话,会导致配置文件下整个目录覆盖,可能会影响到我们并不想更改的默认参数。