歡迎來到Linux教程網
Linux教程網
Linux教程網
Linux教程網
Linux教程網 >> Linux基礎 >> Linux教程 >> Docker在PHP項目開發環境中的應用

Docker在PHP項目開發環境中的應用

日期:2017/2/27 15:48:12   编辑:Linux教程
環境部署是所有團隊都必須面對的問題,隨著系統越來越大,依賴的服務也越來越多,比如我們目前的一個項目就會用到:

- Web服務器:Nginx
- Web程序:PHP + Node
- 數據庫:MySQL
- 搜索引擎:ElasticSearch
- 隊列服務:Gearman
- 緩存服務:Redis + Memcache
- 前端構建工具:npm + bower + gulp
- PHP CLI工具:Composer + PHPUnit

因此團隊的開發環境部署隨之暴露出若干問題:

1. 依賴服務很多,本地搭建一套環境成本越來越高,初級人員很難解決環境部署中的一些問題
2. 服務的版本差異及OS的差異都可能導致線上環境BUG
3. 項目引入新的服務時所有人的環境需要重新配置

對 於問題1,可以用[Vagrant](https://www.vagrantup.com/)這樣的基於虛擬機的項目來解決,團隊成員共享一套開發環境 鏡像。對於問題2,可以引入類似[PHPBrew](https://github.com/phpbrew/phpbrew)這樣的多版本PHP管理工 具來解決。但兩者都不能很好地解決問題3,因為虛擬機鏡像沒有版本管理的概念,當多人維護一個鏡像時,很容易出現配置遺漏或者沖突,一個很大的鏡像傳輸起 來也不方便。

Docker的出現讓上面的問題有了更好的解決方案,雖然個人對於Docker大規模應用到生產環境還持謹慎態度,但如果僅僅考慮測試及開發,私以為Docker的容器化理念已經是能真正解決環境部署問題的銀彈了。

下 面介紹[Docker構建PHP項目開發環境](http://avnpc.com/pages/build-php-develop-env-by- docker)過程中的演進,本文中假設你的操作系統為Linux,已經安裝了Docker,並且已經了解[Docker是什麼] (https://www.docker.com/whatisdocker/),以及[Docker命令行的基礎使用](https: //docs.docker.com/userguide/),如果沒有這些背景知識建議先自行了解。


Hello World

首先還是從一個PHP在Docker容器下的Hello World實例開始。我們准備這樣一個PHP文件`index.php`:

```
<?php
echo "PHP in Docker";
```

然後在同目錄下創建文本文件並命名為`Dockerfile`,內容為:

```
# 從官方PHP鏡像構建
FROM php

# 將index.php復制到容器內的/var/www目錄下
ADD index.php /var/www

# 對外暴露8080端口
EXPOSE 8080

# 設置容器默認工作目錄為/var/www
WORKDIR /var/www

# 容器運行後默認執行的指令
ENTRYPOINT ["php", "-S", "0.0.0.0:8080"]
```

構建這個容器:

```
docker build -t allovince/php-helloworld .
```

運行這個容器

```
docker run -d -p 8080:8080 allovince/php-helloworld
```

查看結果:

```
curl localhost:8080
PHP 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構建了一個開發環境,但覺不覺得有些似曾相識呢。沒錯,其實這種做法和制作一個虛擬機鏡像是差不多的,這種方式存在幾個問題:

- 如果需要驗證某個服務的不同版本,比如測試PHP5.3/5.4/5.5/5.6,就必須准備4個鏡像,但其實每個鏡像只有很小的差異。
- 如果開始新的項目,那麼容器內安裝的服務會不斷膨脹,最終無法弄清楚哪個服務是屬於哪個項目的。


使用單一進程容器

上 面這種將所有服務放在一個容器內的模式有個形象的非官方稱呼: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](https://registry.hub.docker.com/)可以看做是Docker的Github,Docker官方已經准備好了大 量[常用服務的鏡像](https://registry.hub.docker.com/repos/library/?s=stars),同時也有非 常多第三方提交的鏡像。甚至可以基於[Docker-Registry](https://github.com/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容器

MySQL繼承自官方的[MySQL5.6鏡像](https://registry.hub.docker.com/_/mysql),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配置文件`nginx.conf`以及項目的配置文件`default.conf`等。Dockerfile內容為:

```
FROM nginx:1.9

ADD 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](https://registry.hub.docker.com/_/nginx/)是基於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 容器是最復雜的一個,因為在實際項目中,我們很可能需要單獨安裝一些PHP擴展,並用到一些命令行工具,這裡我們以Redis擴展以及Composer來 舉例。首先將項目需要的擴展等文件提前下載到php目錄下,這樣構建時就可以從本地復制而無需每次通過網絡下載,大大加快鏡像構建的速度:

wget https://getcomposer.org/composer.phar -O php/composer.phar
wget 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](https://registry.hub.docker.com/_/php/),這同樣是一個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"]
```

在構建過程中做了這樣一些事情:

1. 復制php和php-fpm配置文件到相應目錄
2. 復制redis擴展源代碼到`/home`
3. 通過`docker-php-ext-install`安裝GD和PDO擴展
4. 通過`pecl`安裝Redis擴展
5. 復制composer到鏡像作為全局指令

按照個人習慣,仍然設置`/opt`目錄作為工作目錄。

這裡有一個細節,在復制tar包文件時,使用的Docker指令是`COPY`而不是`ADD`,這是由於`ADD`指令會[自動解壓`tar`文件](https://docs.docker.com/reference/builder/#add)。

現在終於可以構建+運行了:

docker build -t eva/php ./php
docker run -p 9000:9000 -v ~/opt:/opt -it eva/php

在大多數情況下,Nginx和PHP所讀取的項目源代碼都是同一份,因此這裡同樣掛載本地的`~/opt`目錄,並且綁定9000端口。

### PHP-CLI的實現

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容器

為了方便演示,Redis僅僅作為緩存使用,沒有持久化需求,因此Dockerfile僅有一行

```
FROM redis:3.0
```


容器的連接

上 面已經將原本在一個容器中運行的服務分拆到多個容器,每個容器只運行單一服務。這樣一來容器之間需要能互相通信。Docker容器間通訊的方法有兩種,一 種是像上文這樣將容器端口綁定到一個本地端口,通過端口通訊。另一種則是通過Docker提供的[Linking功能] (https://docs.docker.com/userguide/dockerlinks/),在開發環境下,通過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](https://docs.docker.com/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: 123456

redis:
build: ./redis
ports:
- "6379:6379"

```

然後運行`docker-compose up`,就完成了所有的端口綁定、掛載、鏈接操作。

更復雜的實例

上 面是一個標准PHP項目在Docker環境下的演進過程,實際項目中一般會集成更多更復雜的服務,但上述基本步驟仍然可以通用。比如 [EvaEngine/Dockerfiles](https://github.com/EvaEngine/Dockerfiles)是為了運行我的 開源項目[EvaEngine](http://avnpc.com/pages/eva-engine)准備的基於Docker的開發環 境,EvaEngine依賴了隊列服務Gearman,緩存服務Memcache、Redis,前端構建工具Gulp、Bower,後端Cli工具 Composer、PHPUnit等。具體實現方式可以自行閱讀代碼。

經過團隊實踐,原本大概需要1天時間的環境安裝,切換到 Docker後只需要運行10余條指令,時間也大幅縮短到3小時以內(大部分時間是在等待下載),最重要的是Docker所構建的環境都是100%一致 的,不會有人為失誤引起的問題。未來我們會進一步將Docker應用到CI以及生產環境中。
原文:http://www.wolonge.com/zhuanlan/detail/117441
Copyright © Linux教程網 All Rights Reserved