在 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
是否皆有運行,但結果恐怕會讓人大失所望。
![](/media/1B42613A-7EB3-454D-95DE-BC2274C53610 .png)
怎麼會一個 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。挑選的結果如下:
不過因為這個 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 補充,延伸閱讀