http://blog.chengweichen.com/2016/03/do ... ravel.html
用 Docker 建立 Laravel 的開發環境
在 2016/03/03 及 2016/03/09 的 Laravel news 分別介紹了 laraedit-docker 及 LaraDock。
Laravel News 的這個舉動似乎引爆了 Laravel 圈內的 Docker 熱潮(我自以為引爆啦),所以藉這個機會也來聊一聊「如何用 Docker 建構出適合 Laravel 的開發環境」這題目。
既然目標是建構開發環境,首先當然要先問 Laravel 的開發環境需求為何?
根據官網我們可以得知,Laravel 5.2 對環境的需求為:
根據官網文件
https://laravel.com/docs/5.2#server-requirements
這裡面主要的需求是 PHP 版本及 Extension,Laravel 需求爲 PHP >= 5.5.9。那除了 PHP 之外,我們還要預備哪些軟體?我們只好同時參考一下官方的 homestead 看看它安裝了哪些軟體:
根據官網文件得知 homestead 已安裝上述軟體
將上述內容整理並精簡之後,規劃出我個人認為所需的基本環境需求如下:
PHP 5.5.9
Nginx
Mysql
Beanstalkd
Composer
有了需求,接著就開始用 Docker 建置。
關於 Docker 要如何安裝,可直接參閱 Docker 官方網站,那裡有豐富的文件可以參考,不管你是哪一種 OS 官方都有提供安裝步驟,再不然網路上也有很多教學文章可參考,所以我就不再重複說明了。
如果是 Mac OS 可以考慮使用早期比較多人用的 Boot2docekr 或已被 Docker 官方收購並包入 Docker Toolbox 的 Kitematic,再不然也可以考慮使用 docker-machine。
另外也有人在研究直接在 mac os 上直接使用 docker 的方法,例如這一篇《在 Mac 上使用 Homebrew 安裝 Docker》,不過基本上不管是哪一個作法,其實背後都還是有透過 virtualbox 開啟一台 VM。
(但我的印象中記得有看到已有人成功直接在 mac os 上使用 Docker,不過當下沒有記錄,寫這篇文章時已經找不到資料,搞不好是作夢夢到的,不是現實。)
我個人比較喜歡自己來,所以我都會先用 Vagrant 建立一台 VM,接著在 VM 中安裝並使用 Docker 。
Fat Container
先介紹第一種建置方式,就是將 Container 當成 VM 來使用,有人稱這個為 Fat Container,當然能不能、要不要、建不建議這樣做,已經有很多人討論,像是這篇文章《10 things to avoid in docker containers》就建議你不要在一個 Container 中運行超過一個 process。不過我個人認為,如果只是在開發或測試環境中,使用 Fat Container 也沒什麼不好,但如果要將 Docker 用在 Production,就還是聽一下別人的建議吧。
第一步我們先 pull 所需的 Docker Image,我故意選用 rastasheep/ubuntu-sshd:14.04,原因有二:
它是 ubuntu 14.04
它提供了 ssh 登入
因此基本上 Container 運行之後,就能直接將它假想成一台小 VM 使用,如往常使用 VM 一樣透過 ssh 登入,再安裝軟體並建置環境。步驟說明如下:
首先要 pull Docker Image(其實也不用先 pull,因為如果未曾 pull,那在 docker run 的時候也會幫你自動 pull)
運行 Container
docker run -d --name ubuntu rastasheep/ubuntu-sshd:14.04
-d 代表在 background 運行
--name 則是為此 Container 指定一個特別的 NAME
查看 Container 的 ip(為了要 ssh 登入)
docker inspect --format '{{ .NetworkSettings.IPAddress }}' ubuntu
SSH 登入
ssh root@172.17.0.2
(ip 請換成實際的 ip,另外預設的 root password 是 root)
如此就可以 SSH 登入此 Container 中,繼續安裝其他的 packages。
不過老實說要對 Container 進行操作或下 Command,並不需要透過 SSH 登入,Docker 原本就提供了 docker exec 指令,讓你可以對 Container 內下指令,透過 docker exec 去執行 Container 內的 /bin/bash,就可以讓我們彷彿像登入了 Container 一樣在 Container 之中進行操作。
docker exec -it ubuntu /bin/bash
執行上面的指令,會發現似乎與 SSH 登入一樣,而且還不用先查詢 Container 的 ip,可以直接用 Container 的 NAME 來指定目標。(其實還是有基本前提是該 Container 內確實有 /bin/bash 可以使用。)
既然我們已登入了 Container,剩下的操作就與 VM 上安裝 packages 一樣,因為這是 ubuntu 14.04 所以就用 apt 來安裝 packages:
apt-get update
apt-get upgrade
apt-get install curl
apt-get install php5-cli php5 php-pear php5-mysqlnd php5-json php5-curl php5-gd php5-gmp php5-imap php5-mcrypt
apt-get install php5-fpm
apt-get install nginx
apt-get install mysql-server
apt-get install beanstalkd
curl -sS
https://getcomposer.org/installer | php
mv composer.phar /usr/local/bin/composer
接著嘗試啟動 service
service php5-fpm start
service mysql start
service nginx start
service beanstalkd start
基本上環境就建立完畢,再來我們先登出 Container,回到 VM 再輸入 docker commit 指令。
docker commit ubuntu myubuntu:lnmp
(ubuntu 是 Container 的 NAME,myubuntu:lnmp 是 Image 名稱。)
透過 docker commit 將辛苦安裝好 packages 的 Container 存成 Docker Image,這樣下次就不需要重新安裝,可以由此 Image 來建立全新的乾淨的環境。
上述的作法完全是自行登入 Container 之中並慢慢手動安裝軟體,這樣作法實在太不自動,也不是一般主流建立 Docker image 的作法,所以我們稍微轉換一下,將上述所有的步驟改寫成 Dockerfile。
FROM rastasheep/ubuntu-sshd:14.04
RUN apt-get update
RUN apt-get upgrade -y
RUN apt-get install -y curl
RUN apt-get install -y php5-cli php5 php-pear php5-mysqlnd php5-json php5-curl php5-gd php5-gmp php5-imap php5-mcrypt
RUN apt-get install -y php5-fpm
RUN apt-get install -y nginx
RUN apt-get install -y mysql-server
RUN apt-get install -y beanstalkd
RUN curl -sS
https://getcomposer.org/installer | php
RUN mv composer.phar /usr/local/bin/composer
接著在存放 Dockerfile 的路徑中執行 docker build 指令。如此一來,不用透過人工操作,Docker 會自動執行 Dockerfile 裡面的步驟,幫我建立 Docker Image。
docker build -t myubuntu:lnmp .
接著來驗證成果,透過 docker run 來運行 Contianer,並透過 docker exec 進入 Container 中檢驗一下環境。
docker run -d -i --name myubuntu myubuntu:lnmp
docker exec -it myubuntu:lnmp /bin/bash
在 Container 中輸入 ps 指令,確認一下 Service 是否皆有運行,但結果恐怕會讓人大失所望。
怎麼會一個 Service 都沒運行?不是已經安裝過 Nginx、Mysql、php-fpm 了?一般 VM 一開機不是就會自動運行各種 Service?
這就是 Container 與 VM 其中一個不同之處,這也是剛接觸 Docker 的使用者常會踩到的雷。Container 太方便了,有時會不自主的將 Container 完全視同 VM 看待,但其實不能如此,反而要將 Container 視同閹割版的 VM 看待會比較正確一點。
當 Container 被運行時,它只會執行一個 process,因此若希望 Container 一被啟動時就會同時啟動多個 Service 就需要一些進階技巧,你必須透過 s6 或 supervisor 這類的 process supervision 工具來幫你啟動其他的 Service。換句話說即是當 Container 被運行時,它首先執行的第一個 process 是 process supervision 工具,接著這個 process supervision 工具再去幫你啟動其他的 Service,甚至幫你定期監督且重新啟動 Service。
因為這又是另一個大題目,有興趣深究的可以參考這幾篇文章:
Using Supervisor with Docker
YOUR DOCKER IMAGE MIGHT BE BROKEN without you knowing it (Phusion)
Using Runit in a Docker Container
Docker and S6 – My New Favorite Process Supervisor
Docker: 使用 s6 作為多服務容器的啟動管理程序
本文就先跳過這個題目不處理它。雖然 Service 沒有在 Container 啟動時自動運行,但我們可以比照前面的作法,登入 Container 並手動一一啟動。乍看似乎不太方便,但勉強可以接受啦。
補充說明,如果真的要用此 Container 當作開發環境,我通常會在 docker run 時使用以下的參數:
docker run -d \
--name YourConainerName \
--restart=always \
-p 80:80 \
-p 3306:3306 \
-v HostProjectCodePath:ContainerProjectCodePath \
YourImage
--restart=always
讓 Docker 幫我自動運行及重新啟動 Container
-p 80:80
-p 3306:3306
將需要用到的 port 都對應至 host。
-v HostProjectCodePath:ContainerProjectCodePath
將程式碼放在 Host 的指定路徑,並將它 share 至 Container 中的指定路徑。
當 Container 不需要時,就 docker stop NAME 關掉它,需要時再 docker start NAME 啟動它。如果此 Container 弄髒、弄壞了,就 docker rm -f NAME 刪除它,接著再重新 docker run 產生一個乾淨的新環境。
另外,前述的步驟只是安裝好了環境所需的 packages,讓這個 Container 一運行就如同你開了一安裝好 packages 但尚未設定的 VM 一樣。因此還有許多我沒一一說明的環境設定工作需要接著完成,像是建立 DB、建立 DB 的 User 及設定 Nginx site config 等⋯⋯,但這些就不在本文中詳細說明了,不過接著後續要介紹的其他作法中,剛好會自動處理掉一部份的環境設定,建議您可以繼續看下去。
一個 Container 提供一個 Service
前面介紹了 Fat Container,接著當然是介紹「一個 Container 提供一個 Service 」的作法。
根據前面的環境需求可以得知,我們至少需要運行四個 Service
beanstalkd
mysql
php5-fpm
nginx
於是我們就前往 Docker Hub 為每一個 Service 挑選合適的 Docker Image。挑選的結果如下:
kdihalas/beanstalkd:latest
mysql/mysql-server:5.6
php:5.6-fpm
nginx:1.9.6
不過因為這個 php 的 Docker Image 預設是會缺少一些 Laravel 所需的 php extension,例如 mbstring 及 pdo_mysql(還記得官網上的環境需求嗎?),而且我還需要安裝 Composer。
故此我們需要稍微加工之後才能使用它。我將原本的 php:5.6-fpm 作為 baseimage,再 build 一個自己的版本。
先建立一個空的資料夾,將下面的內容存成檔名 Dockerfile
FROM php:5.6-fpm
RUN docker-php-ext-install -j$(nproc) pdo_mysql
RUN docker-php-ext-install -j$(nproc) mbstring
RUN docker-php-ext-install -j$(nproc) tokenizer
RUN curl -sS
https://getcomposer.org/installer | php
RUN mv composer.phar /usr/local/bin/composer
最後就
docker build -t myphp:5.6-fpm .
-t 是替這個 Image 命名為 myphp:5.6-fpm
接著就按順序啟動各 Container。
第一個是 beanstalkd,這最單純,指令如下:
docker run \
--restart=always \
-d --name dev_queue \
kdihalas/beanstalkd:latest
接著啟動 mysql,指令如下:
docker run \
--restart=always \
-d --name dev_mysql \
-p 3306:3306 \
-e MYSQL_ROOT_PASSWORD=secret \
-e MYSQL_DATABASE=homestead \
-e MYSQL_USER=homestead \
-e MYSQL_PASSWORD=secret \
mysql/mysql-server:5.6
特別說明一下,透過 -e 輸入的 environment variables 會被用來自動設定 mysql 的環境,包含:root 帳號的密碼、新建一個名為 homestead 的 DB、新建一個 User 並設定此 User 的 Password。
至於 -p 則是用來將 VM 的 3306 對應至 Container 的 3306,以便能直接用 Sequel Pro 等軟體直接連進資料庫。
第三個啟動的是 php5-fpm,同樣指令如下:
docker run \
--restart=always \
-d --name dev_phpfpm \
--link dev_mysql:db \
-e DB_HOST=dev_mysql \
--link dev_queue:queue \
-e BEANSTALKD_HOST=dev_queue \
-v /tmp/laravel_project:/var/project \
-w /var/project \
myphp:5.6-fpm
也稍微解說一下,因為 php 程式執行時會需要讀寫 DB,所以當然要與 Mysql Container 連結 --link 在一起,同理也需要與 beanstalkd Container 連接 --link 在一起。而且透過 --link 連結,Docker 會自動幫我在 phpfpm Container 裡的 /etc/hosts 中新增記錄,這樣也比較方便處理 Laravel 中的 DB 連線設定。
Container 中的 /etc/hosts 會多出如圖的記錄
至於 -e 輸入的 environment variables 則是為了自動覆蓋 Laravel 的 .env 設定,讓 Laravel 可以順利連上 Mysql 及 beanstalkd。
因為前面提到的 --link 已經會幫我在 Container 內的 /etc/hosts 新增記錄,因此直接指定 DB_HOST=dev_mysql 就能讓 Laravel 連上 DB。
-v 則是將放在 VM 裡的 Laravel 專案程式碼 share 進 Container 的指定路徑。
最後的 -w 是用來設定 Container 的 workdir,這樣我們透過 docker exec 要對 Container 內下達 Laravel 的 artisan 指令時,就能省去輸入路徑了。
例如:
docker exec dev_phpfpm php artisan
如果沒有設定 workdir,則是
docker exec dev_phpfpm php /var/project/artisan
最後是 Nginx,指令如下:
docker run \
--restart=always \
-d --name dev_nginx \
-p 80:80 \
--link dev_phpfpm:phpfpm \
-v /tmp/laravel_project:/var/project \
-v /tmp/default.conf:/etc/nginx/conf.d/default.conf \
nginx:1.9.6
解說一下,nginx 這裡多了一個 -v,這是要將事先準備好的 nginx site config 放進 Nginx Container,讓它可以確實運行 Laravel 的網站,設定檔如下:
server {
listen 80 default_server;
server_name _;
root /var/project/public;
index index.html index.htm index.php;
charset utf-8;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
error_page 404 /index.php;
sendfile off;
location ~ \.php$ {
fastcgi_split_path_info ^(.+\.php)(/.+)$;
fastcgi_pass dev_phpfpm:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
fastcgi_param DB_HOST dev_mysql;
fastcgi_param BEANSTALKD_HOST dev_queue;
}
location ~ /\.ht {
deny all;
}
}
2016.03.30 補充:更詳細說明 -v /tmp/default.conf:/etc/nginx/conf.d/default.conf,意思是當 Container 運行時,就會把 /tmp/default.conf 掛載至 /etc/nginx/conf.d/default.conf。因為文章只是舉例,所以舉例將 default.conf 放在 VM 的 /tmp/default.conf,一般正常在使用時,default.conf 建議你好好的放在某個路徑保存,不要像舉例這樣放在 /tmp/ 之下,另外再次提醒若有將 degault.conf 放在別的路徑,記得要修改 -v /your/file/path/default.conf
同樣的在這個 Nginx site config 中有特別多了 fastcgi_param 的設定,這也是為了自動覆蓋 Laravel 的 .env 中的設定,讓 Laravel 可以順利連上 Mysql 及 beanstalkd。
啟動完畢當然要測試一下結果,首先在 VM 裡面用 curl 戳一下 localhost,確實有回傳 Laravel 預設的入口頁。
當然在外面用瀏覽器也一樣能順利看到 Laravel 的入口頁。
接著要嘗試執行看看 Laravel 的 artisan。所以一樣透過 docker exec 下達下面的指令:
docker exec dev_phpfpm php artisan migrate
一樣能順利執行指令,並且也驗證了有順利連上 DB。
透過 docker-compose
上面講完了一個接一個啟動 Container 的作法,但要手動 docker run 四次也挺麻煩,所以最後來示範將上面的四個 docker run 指令轉換成 docker-compose.yml,然後透過 docker-compose 來一次啟動它們。
一樣 docker-compose 要怎麼安裝就不說明了,docker 官網一樣都有教學。
我們就直接來看 docker-compose.yml 的內容。
dev_queue:
container_name: dev_queue
restart: always
image: kdihalas/beanstalkd:latest
dev_mysql:
container_name: dev_mysql
restart: always
image: mysql/mysql-server:5.6
ports:
- "3306:3306"
environment:
- MYSQL_ROOT_PASSWORD=secret
- MYSQL_DATABASE=homestead
- MYSQL_USER=homestead
- MYSQL_PASSWORD=secret
dev_phpfpm:
container_name: dev_phpfpm
restart: always
image: myphp:5.6-fpm
links:
- dev_mysql:db
- dev_queue:queue
environment:
- DB_HOST=dev_mysql
- BEANSTALKD_HOST=dev_queue
volumes:
- /tmp/laravel_project:/var/project
working_dir: /var/project
dev_nginx:
container_name: dev_nginx
restart: always
image: nginx:1.9.6
ports:
- "80:80"
links:
- dev_phpfpm:phpfpm
volumes:
- /tmp/default.conf:/etc/nginx/conf.d/default.conf
- /tmp/laravel_project:/var/project
因為是 yaml 檔,其實很容易閱讀,如果再對照前面示範的 docker run 的指令,應該不難看出每一行代表的意義,比較需要說明的是特別用了 contianer_name 這個參數。
當下指令 docker-compose up 時,docker-compose 預設會將它所啟動的 Container 命名為「資料夾 + name + 流水號」,例如:我將 docker-compose.yml 放在名為 aaa 的資料夾中,那麼啟動的 Container 會被依序命名為 aaa_dev_queue_1、aaa_dev_mysql_1、aaa_dev_phpfpm_1 及 aaa_dev_nginx_1。
但這就會與我設定的 DB_HOST=dev_mysql 不吻合,故此要特別加上 contianer_name,告訴 docker-compose 請用我指定的名稱來替 Container 命名。
最後也來驗證一下成果,就讓我們下指令 docker-compose up,應該會看到類似下圖的情況,docker-compose 會陸續幫我們將 Container 啟動
如果不想看這些,在啟動時可以補上 -d 改用 Detached mode,這樣它就會在背景運行了。
小結
本文飛快地介紹了幾種作法,讓你透過 Docker 來建構可運行 Laravel 的開發環境,但其實裡面有很多細節我並沒有一一的在本文中詳細說明,一方面是有太多的細節(雷),再來其實這些細節在你熟悉 Docker 之際,幾乎都一定會踩過,可說是必經之路。
只能說 Docker 使用起來方便,但初期需要投資的學習成本是免不了的,個人在學習過程中覺得有很多的細節(雷)對於 Ops 來說是比較容易理解與解決,但若是完全沒接觸 Ops 的開發者可能就會比較辛苦一點。
像是在取用別人做好的 Docker Image 前,其實需要花一點時間了解一下對方是如何建立 Image,及此 Image 使用上有沒有需要特別注意之處,像前面使用的 Mysql Image,它就很貼心的讓你只要透過 -e 輸入特定的 environment variables,它在啟動時就會自動幫你建立 DB 及新增 User。
我們不難理解 Docker 官方為何會併購 Kitematic ,因為確實需要有更多友善的工具來幫助使用者更容易的使用 Docker。同樣也不難理解為何 Laravel 官方會做出 homestead。若去分析 homestead,你會發現它說穿了也只是一個已預裝好開發環境的 vagrant box ,再搭配特別客制過的 Vagrantfile + scripts,讓你在 vagrant up 時可以很容易的解決 provision 的問題(例如:設定 nginx site config)。透過 homestead 所提供的完善的開發環境及容易使用的特性,再加上 Laravel 官方有不斷維護並更新 box,讓開發者不太需要煩惱建置開發環境的問題。
同理,在 Laravel News 被介紹的 laraedit-docker 及 LaraDock 也是如此,你可以把它想成是該作者建立了一個「工具」,嘗試讓你更方便的操作 Docker、設定環境、Provision ⋯⋯默默替你處理掉許多麻煩事,讓開發者可以快快樂樂用 Docker 作為開發環境。
因此假如你本身已具備 Ops 技能,並且熟悉 Vagrant 及 Docker,你其實也可以做出屬於你自己專屬的 homestead。不過既然都已經有人先做了,又何必重新造輪子呢?只要針對不滿意之處稍微修改一下即可。
本文就到此結束,有機會再來寫一篇文分析 laraedit-docker 及 LaraDock,解釋一下它們到底是怎麼做的,裡面的玄機又是如何。
備註
本文使用的環境與軟體記錄如下:
VM 使用的是 ubuntu 14.04.3 的 vagrant box
安裝的 docker version 爲 1.9.1
安裝的 docker-compose version 爲 1.5.2
2016.4.12 補充,延伸閱讀
Containers are not VMs