用Docker搭建LNMP
程序員經常會說的一句話:在我的機器上是正常的,肯定是你的機器有問題。因此,Docker誕生了,它把應用所需要的一切東西都打包,從而可以很方便地進行部署。
Docker 的主要用途,目前有三大類:
- 提供一次性的環境。比如,本地測試他人的軟件、持續集成的時候提供單元測試和構建的環境。
- 提供彈性的云服務。因為 Docker 容器可以隨開隨關,很適合動態擴容和縮容。
- 組建微服務架構。通過多個容器,一臺機器可以跑多個服務,因此在本機就可以模擬出微服務架構。
下面開始來體驗Docker,操作系統為win10,Docker的安裝請參考官方文檔,這里略過。
Hello World
官方提供了一個程序員最喜歡的hello world鏡像,我們就以此來開始Docker之旅,感受Docker的強大之處吧:
docker run hello-world
如果你看到輸出Hello from Docker!,恭喜你,Docker安裝成功了。
image和container
鏡像和容器可以理解為面向對象里面的類和對象。
上面的例子是直接拿官方的鏡像來跑,我們也可以在官方鏡像的基礎上加工,來構建新的鏡像,當然也可以不基于任何鏡像來構建。
所有內容(代碼、軟件、環境等)都可以通過Dockerfile配置來構建image,通過image可以運行具體的container實例。
查看鏡像列表:docker image ls
查看容器列表:docker container ls
如果要查看完整的列表,在ls后面加個-a選項就好了:docker container ls -a
裝個Linux
下面加點難度,裝個Linux,alpine體積小,很多開源鏡像也是以它為基礎(比如Nginx),很適合用來體驗:
docker run -it --rm alpine
發現run后面多了幾個選項,其中-it其實是-i -t的縮寫,即interactive terminal,交互式終端的意思,不加這個我們是進不到容器的交互式終端的。
加了--rm,表示在容器退出后會自動刪除容器,我們這里是為了體驗,希望用完之后不要占用磁盤空間,所以加了刪除選項,注意這里不是刪除鏡像,刪除的是容器。
現在我們進入到alpine系統的終端了,輸入命令查看系統版本試試:
cat /etc/os-release
體驗完畢,這時候只要輸入exit按下回車就可以退出容器了。
如果想運行容器的時候直接輸出系統版本,可以這么玩:
docker run --rm alpine cat /etc/os-release
在鏡像名后面直接跟上你想運行的命令,輸出系統版本信息以后容器就自動關閉并刪除了。
運行PHP
好了,現在Linux系統裝好了,直覺告訴我,如果要跑PHP,我們就直接進到剛剛那個alpine容器里面安裝就好了,那么不好意思,你的姿勢錯了。先不解釋,先看看正確姿勢:
我們先在CLI模式下體驗PHP,在這之前,我們可以看看官方的php-cli:7.2鏡像的Dockerfile是怎么定義的:
FROM debian:stretch-slim
...此處省略若干行...
FROM指定基于哪個鏡像,也可以不基于任何鏡像:FROM scratch,比如官方的alpine的Dockerfile:
FROM scratch
ADD alpine-minirootfs-3.10.0-x86_64.tar.gz /
CMD ["/bin/sh"]
由此可見,PHP鏡像是建立在debian這個鏡像的基礎上的,所以我們不需要像在虛擬機上面裝PHP一樣,得先用iso安裝一個Linux操作系統。
項目位置在D:\www\php-cli-demo,建立好文件夾以后,在項目下面新建一個index.php文件:
<?php
echo phpversion();
再新建一個Dockerfile文件:
FROM php:7.2-cli
WORKDIR /var/www/php-cli-demo
COPY . .
CMD ["php", "index.php"]
WORKDIR是指定容器的工作目錄,我們需要把當前項目的代碼拷貝到容器的操作系統所在的目錄里,所以我們使用COPY,第一個點的意思是當前目錄,第二個點的意思是容器的當前目錄,意思就是把我win10系統下D:\www\php-cli-demo這個文件夾下的所有文件全部拷貝到容器的Debian系統下的當前目錄(已通過工作目錄指定為/var/www/php-cli-demo)下。
CMD就是容器運行起來以后所要執行的命令了,這里我們希望直接運行這個php文件。
在當前文件夾下,按住Shift鍵,然后按下鼠標右鍵,出現右鍵菜單,這時候出現其中一項“在此處打開Powershell窗口(s)”,這時候我們只需要按下s就可以進入命令行并且直接定位到當前文件夾了。
然后我們根據剛才的Dockerfile配置文件來建立鏡像:
docker build -t php-cli-demo .
t是tag,打標簽的意思。
最后那個點,指定了要打包的是哪個目錄,docker build 命令得知這個路徑后,會將路徑下的所有內容打包,然后上傳給 Docker 引擎。這樣 Docker 引擎收到這個上下文包后,展開就會獲得構建鏡像所需的一切文件,然后Docker引擎在解析上面的COPY指令時,就能知道要COPY哪些文件了,所以這個COPY的第一個參數的路徑是在打包路徑下展開的,是相對路徑。
Docker默認會去找Dockerfile,也可以是別的文件名,例如你的Dockerfile叫build.conf,可以通過-f參數指定:
docker build -t php-cli-demo -f build.conf .
好了,鏡像構建成功了,現在馬上來跑一下吧:
docker run --rm php-cli-demo
可以看到正確輸出了PHP的版本號為:7.2.19。
build的時候把項目代碼一塊打包進image鏡像文件了,那如果我要運行另外一個項目呢?要重新build一遍嗎?其實這個image是可以復用的,只不過在運行的時候指定項目位置就行了:
docker run --rm -v ${PWD}:/var/www/php-cli-demo php-cli-demo
v是volume,數據卷的意思,${PWD}是宿主機當前目錄,冒號后面是容器目錄,意思就是把當前目錄掛載到容器的/var/www/php-cli-demo目錄。
不過不建議這么做,因為鏡像是一層一層做緩存的,所以重復build不會占用太多空間,而且如果鏡像做了改動,會影響到另外一個容器,所以還是單獨build比較好。感興趣的朋友可以自己做一下測試,然后用docker system df查看空間占用情況。
如果像上面這種在基礎鏡像上,沒有涉及到任何加工的,只需要把項目映射到容器就行了,可以直接這么玩:
docker run --rm -v ${PWD}:/var/www/demo -w /var/www/demo php:7.2-cli php index.php
現在,回過頭來分析剛才的問題,為什么不建議在alpine容器里面直接安裝PHP。
鏡像構建時,會一層一層地構建,前一層是后一層的基礎,這樣可以達到復用的效果。
容器的實質是進程,但與直接在宿主執行的進程不同,容器進程運行于屬于自己的獨立的命名空間。因此容器可以擁有自己的 root 文件系統、自己的網絡配置、自己的進程空間,甚至自己的用戶 ID 空間。容器內的進程是運行在一個隔離的環境里,使用起來,就好像是在一個獨立于宿主的系統下操作一樣。這種特性使得容器封裝的應用比直接在宿主運行更加安全。
鏡像使用的是分層存儲,容器也是如此。每一個容器運行時,是以鏡像為基礎層,在其上創建一個當前容器的存儲層。
容器存儲層的生存周期和容器一樣,容器消亡時,容器存儲層也隨之消亡。因此,任何保存于容器存儲層的信息都會隨容器刪除而丟失。
所以,你在alpine容器里面直接安裝PHP,如果不小心把容器刪掉了,下一次再跑的時候,PHP就丟了。而且這是一個黑箱操作,別人并不知道這個容器里面有什么,所以最好還是通過Dockerfile構建鏡像,然后再根據鏡像來跑容器咯。
搭建LNMP
好了,上面的都搞明白之后我們就來跑個真正的PHP項目吧。我們需要用到PHP-FPM,Nginx,MySQL。項目結構是這么安排的:
- docker-php-demo
- app
- index.html
- index.php
- mysql
- Dockerfile
- nginx
- conf.d
- site.conf
- conf.d
- php
- Dockerfile
- app
我們先把Nginx裝起來,跑個靜態頁面試試。直接跑官方鏡像:
docker run --name nginx -d --rm nginx:1.16
打開瀏覽器輸入localhost,就可以看到Welcome to nginx!的靜態頁了。這是Nginx的默認頁面,我們需要加一個專門針對本次項目的配置。進入Nginx容器,查看配置文件路徑:
docker exec -it nginx bash
nginx -V
看到配置文件路徑為:
--conf-path=/etc/nginx/nginx.conf
查看配置文件:
cat /etc/nginx/nginx.conf
可以看到默認的配置還會去加載其他配置文件:
include /etc/nginx/conf.d/*.conf;
所以我們只需要把項目的配置通過-v映射到容器的conf.d目錄下就可以了。把當前的Nginx容器停掉:
docker stop nginx
然后編寫site.conf:
server {
listen 80;
server_name local.docker-php-demo.com;
root /var/www/docker-php-demo;
}
在app目錄放入index.html:
Hello Docker
因為Nginx本身我們不做加工,所以不用Dockerfile,直接用官方鏡像就好了,然后分別把項目和Nginx網站配置都映射過去,在項目根目錄下執行:
docker run --name dpd-nginx -d -v ${PWD}/app:/var/www/docker-php-demo -v ${PWD}/nginx/conf.d:/etc/nginx/conf.d -p 80:80 nginx:1.16
最后在本機配置hosts就大功告成啦,Win + R 打開運行窗口,輸入drivers,打開etc/hosts文件加入一行域名配置:
127.0.0.1 local.docker-php-demo.com
瀏覽器打開local.docker-php-demo.com,到這里,我們就完成了讓Nginx跑靜態頁面的配置了。我們的項目是PHP,把fpm裝起來,然后再讓Nginx連上就行了,在php目錄下編寫Dockerfile:
FROM php:7.2-fpm
RUN cp /usr/local/etc/php/php.ini-development /usr/local/etc/php/php.ini \
&& docker-php-ext-install pdo_mysql
上面我們使用了鏡像提供的安裝擴展的快捷操作方法來安裝pdo_mysql擴展。
在app目錄放入index.php:
<?php
phpinfo();
構建運行:
cd php
docker build -t dpd-php .
cd ..
docker run --name dpd-php -d -v ${PWD}/app:/var/www/docker-php-demo -p 9000:9000 dpd-php
把php和nginx通過網卡連接起來:
docker network create --driver bridge dpd # 新建一個橋接網卡,名字叫dpd
docker network connect dpd dpd-php
docker network connect dpd dpd-nginx
docker network ls # 列出docker當前有哪些網卡
docker network inspect dpd # 查看dpd網卡詳情
修改Nginx配置,把PHP腳本轉發給fpm,fpm地址通過容器名dpd-php就可以識別:
server {
listen 80;
server_name local.docker-php-demo.com;
root /var/www/docker-php-demo;
location ~ \.php$ {
fastcgi_pass dpd-php:9000;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
讓Nginx重新加載配置文件:
docker exec -it dpd-nginx nginx -s reload
到這里,我們就把Nginx 和 php-fpm搭起來了。下面我們再來搭建Mysql,在mysql目錄下新建test.sql,存放表結構和基礎數據:
CREATE TABLE `user` (
`id` int(10) unsigned NOT NULL PRIMARY KEY AUTO_INCREMENT,
`name` varchar(20) NOT NULL
)ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='用戶';
INSERT INTO `user` VALUES (null, 'a1');
Dockerfile:
FROM mysql:5.7
ENV MYSQL_ROOT_PASSWORD=123 MYSQL_DATABASE=test
COPY ./test.sql /var/data/test.sql
構建并啟動容器,這里直接用--network參數把容器加入dpd網絡:
cd mysql
docker build -t dpd-mysql .
docker run --name dpd-mysql -d --network dpd -p 3306:3306 dpd-mysql
進入容器把表和測試數據導入進去:
docker exec -it dpd-mysql bash
mysql -uroot -p
mysql> use test;
mysql> source /var/data/test.sql
修改index.php:
<?php
$dsn = 'mysql:dbname=test;host=dpd-mysql';
$user = 'root';
$password = '123';
try {
$dbh = new PDO($dsn, $user, $password);
$sql = 'SELECT * FROM user WHERE id=?';
$sth = $dbh->prepare($sql);
$sth->execute(array(1));
$result = $sth->fetch(PDO::FETCH_ASSOC);
var_dump($result);
} catch (PDOException $e) {
echo 'Error: ' . $e->getMessage();
}
瀏覽器輸入http://local.docker-php-demo.com/index.php,PHP成功地從數據庫獲取到了用戶數據:
array(2) { ["id"]=> string(1) "1" ["name"]=> string(2) "a1" }

浙公網安備 33010602011771號