This article shares with you the exciting application of Docker in the PHP project development environment and has certain reference value. Friends in need can refer to it. I hope it will be helpful to everyone.
Environment deployment is a problem that all teams must face. As the system becomes larger and larger, the services it depends on There are more and more, for example, one of our current projects will use:
Therefore, the team’s development environment deployment exposed several problems:
For problem 1, it can be solved with a virtual machine-based project like Vagrant, where team members share a set of development environment images. For problem 2, you can introduce a multi-version PHP management tool like PHPBrew to solve it. But neither can solve problem 3 very well, because virtual machine images do not have the concept of version management. When multiple people maintain an image, configuration omissions or conflicts can easily occur, and it is inconvenient to transfer a large image.
The emergence of Docker has provided a better solution to the above problems. Although I am still cautious about the large-scale application of Docker to production environments, if I only consider testing and development, I think Docker’s containerization The concept is already a silver bullet that can truly solve the problem of environment deployment.
The following introduces the evolution of Docker in building a PHP project development environment. This article assumes that your operating system is Linux, you have installed Docker, and you already understand what Docker is and the basic use of the Docker command line. If If you don’t have this background knowledge, it is recommended that you understand it on your own first.
First, start with a Hello World instance of PHP in a Docker container. We prepare such a PHP file index.php
:
<?phpecho "PHP in Docker";
Then create a text file in the same directory and name it Dockerfile
, with the content:
# 从官方PHP镜像构建FROM php# 将index.php复制到容器内的/var/www目录下ADD index.php /var/www/# 对外暴露8080端口EXPOSE 8080# 设置容器默认工作目录为/var/wwwWORKDIR /var/www/# 容器运行后默认执行的指令ENTRYPOINT ["php", "-S", "0.0.0.0:8080"]
Build this container:
docker build -t allovince/php-helloworld .
运行这个容器
docker run -d -p 8080:8080 allovince/php-helloworld
查看结果:
curl localhost:8080PHP in Docker
这样我们就创建了一个用于演示PHP程序的Docker容器,任何安装过Docker的机器都可以运行这个容器获得同样的结果。而任何有上面的php文件和Dockerfile的人都可以构建出相同的容器,从而完全消除了不同环境,不同版本可能引起的各种问题。
想象一下程序进一步复杂,我们应该如何扩展呢,很直接的想法是继续在容器内安装其他用到的服务,并将所有服务运行起来,那么我们的Dockerfile很可能发展成这个样子:
FROM php ADD index.php /var/www/# 安装更多服务RUN apt-get install -y \ mysql-server \ nginx \ php5-fpm \ php5-mysql# 编写一个启动脚本启动所有服务ENTRYPOINT ["/opt/bin/php-nginx-mysql-start.sh"]
虽然我们通过Docker构建了一个开发环境,但觉不觉得有些似曾相识呢。没错,其实这种做法和制作一个虚拟机镜像是差不多的,这种方式存在几个问题:
上面这种将所有服务放在一个容器内的模式有个形象的非官方称呼:Fat Container。与之相对的是将服务分拆到容器的模式。从Docker的设计可以看到,构建镜像的过程中可以指定唯一一个容器启动的指令,因此Docker天然适合一个容器只运行一种服务,而这也是官方更推崇的。
分拆服务遇到的第一个问题就是,我们每一个服务的基础镜像从哪里来?这里有两个选项:
选项一、 统一从标准的OS镜像扩展,比如下面分别是Nginx和MySQL镜像
FROM ubuntu:14.04 RUN apt-get update -y && apt-get install -y nginx
FROM ubuntu:14.04 RUN apt-get update -y && apt-get install -y mysql
这种方式的优点在于所有服务可以有一个统一的基础镜像,对镜像进行扩展和修改时可以使用同样的方式,比如选择了ubuntu,就可以使用apt-get
指令安装服务。
问题在于大量的服务需要自己维护,特别是有时候需要某个服务的不同版本时,往往需要直接编译源码,调试维护成本都很高。
选项二、 直接从Docker Hub继承官方镜像,下面同样是Nginx和MySQL镜像
FROM nginx:1.9.0
FROM mysql:5.6
Docker Hub可以看做是Docker的Github,Docker官方已经准备好了大量常用服务的镜像,同时也有非常多第三方提交的镜像。甚至可以基于Docker-Registry项目在短时间内自己搭建一个私有的Docker Hub。
基于某个服务的官方镜像去构建镜像,有非常丰富的选择,并且可以以很小的代价切换服务的版本。这种方式的问题在于官方镜像的构建方式多种多样,进行扩展时需要先了解原镜像的Dockerfile
。
出于让服务搭建更灵活的考虑,我们选择后者构建镜像。
为了分拆服务,现在我们的目录变为如下所示结构:
~/Dockerfiles ├── mysql │ └── Dockerfile ├── nginx │ ├── Dockerfile │ ├── nginx.conf │ └── sites-enabled │ ├── default.conf │ └── evaengine.conf ├── php │ ├── Dockerfile │ ├── composer.phar │ ├── php-fpm.conf │ ├── php.ini │ ├── redis.tgz └── redis └── Dockerfile
即为每个服务创建单独文件夹,并在每个服务文件夹下放一个Dockerfile。
MySQL继承自官方的MySQL5.6镜像,Dockerfile仅有一行,无需做任何额外处理,因为普通需求官方都已经在镜像中实现了,因此Dockerfile的内容为:
FROM mysql:5.6
在项目根目录下运行
docker build -t eva/mysql ./mysql
会自动下载并构建镜像,这里我们将其命名为eva/mysql
。
由于容器运行结束时会丢弃所有数据库数据,为了不用每次都要导入数据,我们将采用挂载的方式持久化MySQL数据库,官方镜像默认将数据库存放在/var/lib/mysql
,同时要求容器运行时必须通过环境变量设置一个管理员密码,因此可以使用以下指令运行容器:
docker run -p 3306:3306 -v ~/opt/data/mysql:/var/lib/mysql -e MYSQL_ROOT_PASSWORD=123456 -it eva/mysql
通过上面的指令,我们将本地的3306端口绑定到容器的3306端口,将容器内的数据库持久化到本地的~/opt/data/mysql
,并且为MySQL设置了一个root密码123456
Nginx目录下提前准备了Nginx配置文件nginx.conf
以及项目的配置文件default.conf
等。Dockerfile内容为:
FROM nginx:1.9ADD nginx.conf /etc/nginx/nginx.conf ADD sites-enabled/* /etc/nginx/conf.d/ RUN mkdir /opt/htdocs && mkdir /opt/log && mkdir /opt/log/nginx RUN chown -R www-data.www-data /opt/htdocs /opt/log VOLUME ["/opt"]
由于官方的Nginx1.9是基于Debian Jessie的,因此首先将准备好的配置文件复制到指定位置,替换镜像内的配置,这里按照个人习惯,约定/opt/htdocs
目录为Web服务器根目录,/opt/log/nginx
目录为Nginx的Log目录。
同样构建一下镜像
docker build -t eva/nginx ./nginx
并运行容器
docker run -p 80:80 -v ~/opt:/opt -it eva/nginx
注意我们将本地的80端口绑定到容器的80端口,并将本地的~/opt
目录挂载到容器的/opt
目录,这样就可以将项目源代码放在~/opt
目录下并通过容器访问了。
PHP容器是最复杂的一个,因为在实际项目中,我们很可能需要单独安装一些PHP扩展,并用到一些命令行工具,这里我们以Redis扩展以及Composer来举例。首先将项目需要的扩展等文件提前下载到php目录下,这样构建时就可以从本地复制而无需每次通过网络下载,大大加快镜像构建的速度:
wget https://getcomposer.org/composer.phar -O php/composer.pharwget https://pecl.php.net/get/redis-2.2.7.tgz -O php/redis.tgz
php目录下还准备好了php配置文件php.ini
以及php-fpm.conf
,基础镜像我们选择的是PHP 5.6-FPM,这同样是一个Debian Jessie镜像。官方比较亲切的在镜像内部准备了一个docker-php-ext-install
指令,可以快速安装如GD、PDO等常用扩展。所有支持的扩展名称可以通过在容器内运行docker-php-ext-install
获得。
来看一下Dockerfile
FROM php:5.6-fpm ADD php.ini /usr/local/etc/php/php.ini ADD php-fpm.conf /usr/local/etc/php-fpm.conf COPY redis.tgz /home/redis.tgz RUN docker-php-ext-install gd \ && docker-php-ext-install pdo_mysql \ && pecl install /home/redis.tgz && echo "extension=redis.so" > /usr/local/etc/php/conf.d/redis.ini ADD composer.phar /usr/local/bin/composer RUN chmod 755 /usr/local/bin/composer WORKDIR /opt RUN usermod -u 1000 www-data VOLUME ["/opt"]
在构建过程中做了这样一些事情:
/home
docker-php-ext-install
安装GD和PDO扩展pecl
安装Redis扩展按照个人习惯,仍然设置/opt
目录作为工作目录。
这里有一个细节,在复制tar包文件时,使用的Docker指令是COPY
而不是ADD
,这是由于ADD
指令会自动解压tar
文件。
现在终于可以构建+运行了:
docker build -t eva/php ./php docker run -p 9000:9000 -v ~/opt:/opt -it eva/php
在大多数情况下,Nginx和PHP所读取的项目源代码都是同一份,因此这里同样挂载本地的~/opt
目录,并且绑定9000端口。
php容器除了运行php-fpm外,还应该作为项目的php cli使用,这样才能保证php版本、扩展以及配置文件保持一致。
例如在容器内运行Composer,可以通过下面的指令实现:
docker run -v $(pwd -P):/opt -it eva/php composer install --dev -vvv
这样在任意目录下运行这行指令,等于动态将当前目录挂载到容器的默认工作目录并运行,这也是PHP容器指定工作目录为/opt
的原因。
同理还可以实现phpunit、npm、gulp等命令行工具在容器内运行。
为了方便演示,Redis仅仅作为缓存使用,没有持久化需求,因此Dockerfile仅有一行
FROM redis:3.0
上面已经将原本在一个容器中运行的服务分拆到多个容器,每个容器只运行单一服务。这样一来容器之间需要能互相通信。Docker容器间通讯的方法有两种,一种是像上文这样将容器端口绑定到一个本地端口,通过端口通讯。另一种则是通过Docker提供的Linking功能,在开发环境下,通过Linking通信更加灵活,也能避免端口占用引起的一些问题,比如可以通过下面的方式将Nginx和PHP链接起来:
docker run -p 9000:9000 -v ~/opt:/opt --name php -it eva/php docker run -p 80:80 -v ~/opt:/opt -it --link php:php eva/nginx
在一般的PHP项目中,Nginx需要链接PHP,而PHP又需要链接MySQL,Redis等。为了让容器间互相链接更加容易管理,Docker官方推荐使用Docker-Compose完成这些操作。
用一行指令完成安装
pip install -U docker-compose
然后在Docker项目的根目录下准备一个docker-compose.yml文件,内容为:
nginx: build: ./nginx ports: - "80:80" links: - "php" volumes: - ~/opt:/opt php: build: ./php ports: - "9000:9000" links: - "mysql" - "redis" volumes: - ~/opt:/opt mysql: build: ./mysql ports: - "3306:3306" volumes: - ~/opt/data/mysql:/var/lib/mysql environment: MYSQL_ROOT_PASSWORD: 123456redis: build: ./redis ports: - "6379:6379"
然后运行docker-compose up
,就完成了所有的端口绑定、挂载、链接操作。
上面是一个标准PHP项目在Docker环境下的演进过程,实际项目中一般会集成更多更复杂的服务,但上述基本步骤仍然可以通用。比如EvaEngine/Dockerfiles是为了运行我的开源项目EvaEngine准备的基于Docker的开发环境,EvaEngine依赖了队列服务Gearman,缓存服务Memcache、Redis,前端构建工具Gulp、Bower,后端Cli工具Composer、PHPUnit等。具体实现方式可以自行阅读代码。
经过团队实践,原本大概需要1天时间的环境安装,切换到Docker后只需要运行10余条指令,时间也大幅缩短到3小时以内(大部分时间是在等待下载),最重要的是Docker所构建的环境都是100%一致的,不会有人为失误引起的问题。未来我们会进一步将Docker应用到CI以及生产环境中。
本文首发于我在卧龙阁的专栏PHP与创业的那些事儿,转载请保留。
推荐学习:《PHP视频教程》
The above is the detailed content of The exciting application of Docker in PHP project development environment (share). For more information, please follow other related articles on the PHP Chinese website!