Docker在PHP專案開發環境中的應用
環境部署是所有團隊都必須面對的問題,隨著系統越來越大,依賴的服務也越來越多,比如我們目前的一個專案就會用到:
- Web伺服器:Nginx
- Web程式:PHP + Node
- 資料庫:MySQL
- 搜尋引擎:ElasticSearch
- 佇列服務:Gearman
- 快取服務:Redis + Memcache
- 前端構建工具:npm + bower + gulp
- PHP CLI工具:Composer + PHPUnit
因此團隊的開發環境部署隨之暴露出若干問題:
- 依賴服務很多,本地搭建一套環境成本越來越高,初級人員很難解決環境部署中的一些問題
- 服務的版本差異及OS的差異都可能導致線上環境BUG
- 專案引入新的服務時所有人的環境需要重新配置
對於問題1,可以用Vagrant這樣的基於虛擬機器的專案來解決,團隊成員共享一套開發環境映象。對於問題2,可以引入類似PHPBrew這樣的多版本PHP管理工具來解決。但兩者都不能很好地解決問題3,因為虛擬機器映象沒有版本管理的概念,當多人維護一個映象時,很容易出現配置遺漏或者衝突,一個很大的映象傳輸起來也不方便。
Docker的出現讓上面的問題有了更好的解決方案,雖然個人對於Docker大規模應用到生產環境還持謹慎態度,但如果僅僅考慮測試及開發,私以為Docker的容器化理念已經是能真正解決環境部署問題的銀彈了。
下面介紹Docker構建PHP專案開發環境過程中的演進,本文中假設你的作業系統為Linux,已經安裝了Docker,並且已經瞭解Docker是什麼,以及Docker命令列的基礎使用,如果沒有這些背景知識建議先自行了解。
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可以看做是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容器
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配置檔案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是基於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,這同樣是一個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"]
在構建過程中做了這樣一些事情:
- 複製php和php-fpm配置檔案到相應目錄
- 複製redis擴充套件原始碼到
/home
- 通過
docker-php-ext-install
安裝GD和PDO擴充套件 - 通過
pecl
安裝Redis擴充套件 - 複製composer到映象作為全域性指令
按照個人習慣,仍然設定/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-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功能,在開發環境下,通過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: 123456
redis:
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與創業的那些事兒,轉載請保留。
相關文章
- 使用 Docker 開發 PHP 專案(五):生產環境DockerPHP
- 用 Docker 構建 PHP 開發環境DockerPHP開發環境
- PHP開發環境 03 - 使用KFKDock搭建PHP專案環境PHP開發環境
- VSCode + Docker 的 PHP 開發環境VSCodeDockerPHP開發環境
- 一鍵搭建基於docker的nodejs、php開發環境 (開源專案)DockerNodeJSPHP開發環境
- Docker配置PHP開發環境DockerPHP開發環境
- 00 在Windows環境中開發Cordova專案的準備工作Windows
- Windows 下如何使用 Linux 環境開發 PHP 專案!WindowsLinuxPHP
- 用 Docker 搭建 Laravel 開發環境DockerLaravel開發環境
- 徒手用 Docker 構建自己的 PHP 開發環境DockerPHP開發環境
- 在Docker容器中執行GUI圖形應用的開源專案DockerGUI
- 用 Docker 製作的 Laravel 開發環境DockerLaravel開發環境
- 用docker安裝laravel的開發環境DockerLaravel開發環境
- 避坑!用 Docker 搞定 PHP 開發環境搭建(Mac、Docker、Nginx、PHP-FPM、XDebug、PHPStorm、VSCode)DockerPHP開發環境MacNginxORMVSCode
- PHP開發環境PHP開發環境
- 專案開發中的pro、pre、test、dev環境及SpringBoot多環境配置devSpring Boot
- 專案經理在敏捷環境中的作用敏捷
- django專案開發環境搭建Django開發環境
- 在 macOS Catalina 10.15 搭建 PHP 開發環境MacPHP開發環境
- docker搭建php環境DockerPHP
- 使用 Docker 開發 PHP 專案(二):配置DockerPHP
- 使用 Docker 開發 PHP 專案(四):CLIDockerPHP
- 企業應用架構研究系列十九:Docker開發環境應用架構Docker開發環境
- 專業的PHP/MySQL開發環境:MAMP Pro for MacPHPMySql開發環境Mac
- 基於Docker搭建PHP+Nginx+MySQL開發環境DockerPHPNginxMySql開發環境
- 自建 Laravel 的 Docker 開發環境LaravelDocker開發環境
- Docker下的Nacos環境開發Docker
- 使用 Docker 開發 PHP 專案(一):安裝DockerPHP
- 專案開發中,如何使用eolinker進行環境管理:開發、測試和生產環境
- Docker Contos 搭建 PHP 環境DockerPHP
- docker構建php環境DockerPHP
- 擁抱 Docker!Laravel 老專案開發環境從 Homestead 遷移到 SailDockerLaravel開發環境AI
- web開發實戰教程:Apache Shiro在web專案中的應用WebApache
- MacOS PHP 開發環境配置MacPHP開發環境
- Mac 搭建 PHP 開發環境MacPHP開發環境
- PHP 線上開發環境PHP開發環境
- 快速搭建本地docker的開發環境Docker開發環境
- 基於Docker的LNMP開發環境DockerLNMP開發環境
- 自己擼一個 LaraDock(使用 Docker LNMP 部署 PHP 開發環境)DockerLNMPPHP開發環境