相信有那麼一天,我們將可以像畢凱艦長一樣用嘴巴叫所有主機做事!


Cheng Wei Chen



用 Docker 建立 Laravel 的開發環境

2016/03/032016/03/09Laravel news  分別介紹了 laraedit-dockerLaraDock

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 ToolboxKitematic,再不然也可以考慮使用 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 登入,再安裝軟體並建置環境。步驟說明如下:
  1. 首先要 pull Docker Image(其實也不用先 pull,因為如果未曾 pull,那在 docker run 的時候也會幫你自動 pull
  2. 運行 Container
    docker run -d --name ubuntu rastasheep/ubuntu-sshd:14.04
    -d 代表在 background 運行
    --name 則是為此 Container 指定一個特別的 NAME
  3. 查看 Container 的 ip(為了要 ssh 登入)
    docker inspect --format '{{ .NetworkSettings.IPAddress }}' ubuntu
  4. 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 就需要一些進階技巧,你必須透過 s6supervisor 這類的 process supervision 工具來幫你啟動其他的 Service。換句話說即是當 Container 被運行時,它首先執行的第一個 process 是 process supervision 工具,接著這個 process supervision 工具再去幫你啟動其他的 Service,甚至幫你定期監督且重新啟動 Service

因為這又是另一個大題目,有興趣深究的可以參考這幾篇文章:

本文就先跳過這個題目不處理它。雖然 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-dockerLaraDock 也是如此,你可以把它想成是該作者建立了一個「工具」,嘗試讓你更方便的操作 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 補充,延伸閱讀




2 則留言:

  1. I cannot launch the application successful.
    it always warn that the mounting problem of the default.conf
    I try to mount the folder which contain the default.conf to /etc/nginx/conf.d
    but also fail

    below are the error message

    ERROR: for dev_nginx Cannot start service dev_nginx: oci runtime error: rootfs_linux.go:53: mounting "/mnt/sda1/var/lib/docker/aufs/mnt/e416de58330582872aa922b5e4677d02261eb4923a50096441f38ae8b4270f6
    3/etc/nginx/conf.d/default.conf" to rootfs "/mnt/sda1/var/lib/docker/aufs/mnt/e416de58330582872aa922b5e4677d02261eb4923a50096441f38ae8b4270f63" caused "not a directory"
    [31mERROR [0m: Encountered errors while bringing up the project.

    回覆刪除
    回覆
    1. Sorry.

      From the message you provided, I was not sure what caused your problem.
      Perhaps it should be docker or your operating environment caused the problem.

      Because of your message, I tested my steps again.
      And I could run nginx container normally.

      For reference.
      I used a clear VM with ubuntu 14.04.5
      docker version = 1.12.1
      docker-compose version = 1.8.0

      刪除

不歡迎留言打廣告,所以有進行留言管理,敬請見諒。