Docker建立Laravel用的LNMP环境

目录
  1. 1. 前置作業
  2. 2. 編寫 PHP 的 Dockerfile
  3. 3. 新增 Nginx 設定檔
  4. 4. 編寫 docker-compose
  5. 5. 執行 docker-compose
  6. 6. 建立資料庫與資料庫使用者
  7. 7. 用 container 裡的 composer 建立 Laravel 專案
  8. 8. 開啟 Laravel 的測試頁
  9. 9. 測試 Laravel 與 MariaDB 的連線

雖然標題上是寫 Laravel 用的環境,不過這套環境也適用於 WordPress。

LNMP 是一般對 Linux + Nginx + MySQL(MariaDB)+ PHP 的簡稱,不過用 Docker 來做的話應該要稱做 NMP才對。本次的範例在任何的 Linux Distro 底下應該都會有一樣的效果,理論上在 MacOS 也差不多,不過 Mac 也許還是會有此許的不同,這點我就沒有再多做測試。

會寫這篇的原因除了想開始學習 PHP 與 Laravel 之外,也是想多熟練 Docker,畢竟 config 或 yaml 寫好之後,帶到哪都可輕鬆重現環境,這種 IaC(Infrastructure-as-Code)加上版控的作法最近非常吸引我(就是喜歡不刺眼的黑黑畫面!)

扣除建立 Laravel 練習環境這件事,這篇文章其實也等同於用 Docker 建立一個普通的 LNMP Web Server,不過因為是練習用,所以 nginx 或 php 的一些細項設定就不太會提到了。

前置作業

首先在系統裡建立一個新資料夾 lnmp,裡面再分別建立四個資料夾,這邊將資料夾命名為:php、ngnix、mariadb 與 projects。

1
2
3
4
5
6
7
8
9
10
11
12
mkdir lnmp && cd lnmp

mkdir nginx php mariadb projects

tree

# 執行結果
.
├── mariaDB
├── nginx
├── php
└── projects

4 個資料夾的用途如下:

  • mariaDB:
    給 MariaDB 的 container 掛載用,存放 data base 的資料,也可以選擇不掛載
  • nginx:
    給 nginx container 掛載用,存放 nginx conf
  • php:
    放自行 build 的 php Dockerfile
  • projects:
    統一將 Laravel 或 WordPress 的資料夾放在這邊

使用 tree 指令是為了方便給大家看資料與資料夾間的關聯,這個指令通常不會內建在系統中,也不是很必要,可依個人喜好自行安裝

編寫 PHP 的 Dockerfile

PHP 官方的 Docker Image 基底為 Debian,另外也有出基於 Alpine 的版本,Alpine 做出來的 image 會比較輕量,另外我們的 web servce 是使用 nginx,所以就要選 php-fpm 的 image,而不是 php image。

本次的 Dockerfile 使用官方的 php:7.4-fpm-alpine 為底,如果只是想基本的練練 php,那直接使用官方的 php-fpm image 就可以了,但在 Laravel 的官網中有提到,7.x 版的 Laravel 需要許多額外的套件:

1
2
3
4
5
6
7
8
9
- BCMath
- Ctype
- Fileinfo
- JSON
- Mbstring
- OpenSSL
- PDO
- Tokenizer
- XML

這些套件在公版 php image 裡是沒有的,所以我們必須自己寫 Dockerfile,將這些套件包進 image。

另外 WordPress 的官網也有提到所需的套件如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
- curl
- dom
- exif
- fileinfo
- hash
- imagick
- json
- mbstring
- mysqli
- openssl
- pcre
- sodium
- xml
- zip
- bcmath
- filter
- gd
- iconv
- intl
- mcrypt
- simplexml
- xmlreader
- zlib

上面大多 module 是預設已經有的,或是與 Laravel 有套件有重覆到,所以這次包的 image 就是以上面的 module 為目標,加上 Laravel 會用到的 composer 把它們全部包起來,Dockerfile 如下:

1
vim ./php/Dockerfile
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
# 注:此处只能使用PHP的fpm版本,如果不带fmp的话,nginx无法执行php脚本。至于能不能执行php-cli的定时任务,待验证。
FROM php:7.4-fpm-alpine
MAINTAINER Wade

# Install gd, iconv, mbstring, mysql, soap, sockets, zip, and zlib extensions
# $PHPIZE_DEPS include autoconf, make...etc
# see example at https://hub.docker.com/_/php/
RUN apk add --update \
$PHPIZE_DEPS \
freetype-dev \
git \
libjpeg-turbo-dev \
libpng-dev \
libxml2-dev \
libzip-dev \
openssh-client \
php7-json \
php7-openssl \
php7-pdo \
php7-pdo_mysql \
php7-session \
php7-simplexml \
php7-tokenizer \
php7-xml \
imagemagick \
imagemagick-libs \
imagemagick-dev \
php7-imagick \
php7-pcntl \
php7-zip \
sqlite \
&& docker-php-ext-install iconv soap sockets exif bcmath pdo_mysql pcntl \
&& docker-php-ext-configure gd --with-jpeg --with-freetype \
&& docker-php-ext-install gd \
&& docker-php-ext-install zip

# add mysqli
RUN printf "\n" | docker-php-ext-install mysqli

# add intl
RUN printf "\n" | apk add --update \
icu-dev \
&& docker-php-ext-configure intl \
&& docker-php-ext-install intl

# add mcrypt
RUN printf "\n" | apk add --update \
libmcrypt-dev \
&& pecl install \
mcrypt && \
docker-php-ext-enable mcrypt

# add imagick
RUN printf "\n" | pecl install \
imagick && \
docker-php-ext-enable --ini-name 20-imagick.ini imagick

# add pcov
RUN printf "\n" | pecl install \
pcov && \
docker-php-ext-enable pcov

# add composer
RUN php -r "copy('https://getcomposer.org/installer', 'composer-setup.php');" \
&& php composer-setup.php \
&& php -r "unlink('composer-setup.php');" \
&& mv composer.phar /usr/bin/composer

# setup timezone
RUN sed -i 's/;date.timezone =/date.timezone = "Asia\/Shanghai"/g' /etc/php7/php.ini

# change www-data's uid and gid for laravel folder permisstion
RUN apk --no-cache add shadow && \
usermod -u 1000 www-data && \
groupmod -g 1000 www-data

#EOF

這份 Dockerfile 是以 GitHub 上,jpswade 的範例為參考再加上缺少的套件。下面用了很多獨立的行來安裝單一的套件,是我在範例外新增的,其實也可以合併成一個,或合併到最上面的 RUN,看個人的偏好(分開寫的好處是比較容易 debug),可以等內容都很確定之後再重寫的乾淨點。

另外最底下把 image 裡 www-data 這個 user 的 uid 改成了 1000,這是為了之後 container 可以正常的存取檔案,而不受到檔案權限的阻擋。

新增 Nginx 設定檔

在 nginx 裡建立一個 conf.d 的資料夾,再新增設定檔如下:

1
2
3
mkdir ./nginx/conf.d

vim ./nginx/conf.d/laravel.conf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
server {
listen 80;
server_name my-lab;
root /my_projects;

add_header X-Frame-Options "SAMEORIGIN";
add_header X-Content-Type-Options "nosniff";

index index.php;

charset utf-8;

location / {
try_files $uri $uri/ /index.php?$query_string;
}

location = /favicon.ico { access_log off; log_not_found off; }
location = /robots.txt { access_log off; log_not_found off; }

error_page 404 /index.php;

location ~ \.php$ {
fastcgi_pass php:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
#fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
include fastcgi_params;
}

location ~ /\.(?!well-known).* {
deny all;
}
}

基本上是直接拿 Laravel 官網的 conf 檔來改,要注意的地方如下:

  • server_name:
    自訂網站的域名,即使沒有申請,也建議設一下,之後再修改 hosts 檔就可以模擬真實的情況。
  • root:
    網頁檔存放的路徑,路徑可自訂,之後啟動 container 時要把本機的 projects 資料夾掛載到這邊。
  • fastcgi_pass:
    負責解析 php 的 service,一般的 LNMP Server 在這邊可能會是本機 127.0.0.1:9000,不過我們的 php 會放在別的 container 裡,並且會將該 php container 命名為 php,之後 docker-compose 啟動的每個 container 都能互相解析彼此的 host name,所以這邊設 php:9000 即可。

編寫 docker-compose

接著用 docker-compose 將所有的 service 都建構出來執行,內容如下:

1
vim docker-compose.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
services:

# php-fpm 7.3
php:
build:
context: .
dockerfile: ./php/Dockerfile
container_name: php
restart: unless-stopped
volumes:
- ./projects:/my_projects

# nginx
nginx:
image: nginx:latest
container_name: nginx
restart: unless-stopped
ports:
- 80:80
volumes:
- ./nginx/conf.d:/etc/nginx/conf.d
- ./projects:/my_projects
environment:
- TZ=Asia/Shanghai

# MariaDB
mariadb:
image: mariadb
container_name: mariadb
restart: unless-stopped
ports:
- 3306:3306
volumes:
- ./mariadb:/var/lib/mysql
environment:
MYSQL_ROOT_PASSWORD: abc123
  • php:
    - build 的 dockerfile 裡,指定要用哪個 Dockerfile 來 build php 的 image
    - 在 volumes 這邊,必須要跟 nginx 一起掛載同個目錄,本例為本機上的 projects 資料夾
  • nginx:
    - ports,基本就是 80:80 的 mapping 或是加個 443:443
    - volumes 部分,將本機上的 conf 與 projects 掛載到 container 中
  • mariadb:
    - ports 用預設的 3306:3306
    - volumes 可將 container 裡的 DB 資料放到本機上,不設定的話,container 刪除後 DB 的資料也會不見,如果每次都想啟個乾淨環境的話就不用設這個
    - environments 這邊設定 DB 的 root 密碼

執行 docker-compose

docker-compose.yml 寫好後,直接執行即可

1
2
3
4
5
6
7
8
docker-compose up -d

# 執行結果

Creating network "lnmp_default" with the default driver
Creating php ... done
Creating nginx ... done
Creating mariadb ... done

第一次執行時,php 的 image 需要 build ,所以要花比較多的時間,完成後可用指令查看 container 是否有成功的執行

1
2
3
4
5
6
7
8
docker ps

# 執行結果

CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
9860ea671988 nginx:latest "/docker-entrypoint.…" About a minute ago Up 58 seconds 0.0.0.0:80->80/tcp, :::80->80/tcp nginx
b29ff7a5a9cb lnmp_php "docker-php-entrypoi…" About a minute ago Up 58 seconds 9000/tcp php
ed9dbd32714f mariadb "docker-entrypoint.s…" About a minute ago Up 58 seconds 0.0.0.0:3306->3306/tcp, :::3306->3306/tcp mariadb

启动出现问题:docker-compose生成的容器立刻退出,exited with code 0
原因:docker容器执行任务完成后就会处于exited状态
解决:
Docker镜像的缺省命令是bash,如果不加 -it,bash命令执行了自动会退出,加-it后docker命令会为容器分配一个伪终端,并接管其stdin/stdout支持交互操作,这时候bash命令不会自动退出
像不使用docker-compose,我们会执行类似如下的命令
docker run -it --name node node
但docker-compose需要额外配置下,需要在docker-compose.yml中包含以下行:

1
2
3
4
nginx:
...
stdin_open: true
tty: true

第一个对应于docker run中的 -i,第二个对应于 -t

建立資料庫與資料庫使用者

docker-compose 成功啟動後,表示 php、nginx、MariaDB 應該有順利運行了,再我們就建個資料庫給 Laravel 使用

首先用 root 登入 container 裡的 DB

1
docker exec -it mariadb mysql -u root -p

執行後會提示要輸入密碼,打上之前在 docker-compose.yml 裡定義好的 root 密碼

建立名為 laravel 的資料庫

1
CREATE DATABASE laravel;

新增使用者 laravel_user,密碼為 abcd1234

1
CREATE USER 'laravel_user' IDENTIFIED BY 'abcd1234';

賦予使用者 laravel_user 存取 laravel 資料庫的權限,最後離開 DB

1
2
3
4
5
GRANT ALL PRIVILEGES ON laravel.* TO 'laravel_user';

FLUSH PRIVILEGES;

quit;

用 container 裡的 composer 建立 Laravel 專案

Laravel 可以從官網直接下載,也可以使用 composer 來安裝,既然我們有把 composer 這個套件包進 php 的 image 裡,就試試看用這種方式安裝吧!

首先進到 projects 目錄中

1
cd projects

查看 php image 的名稱

1
2
3
4
5
6
7
8
9
docker images

# 執行結果,lnmp_php 就是剛才自製的 php image 名稱

REPOSITORY TAG IMAGE ID CREATED SIZE
lnmp_php latest e282a489e005 12 hours ago 518MB
php 7.4-fpm-alpine 5ae3ea657944 40 hours ago 82.9MB
mariadb latest fd17f5776802 4 days ago 409MB
nginx latest 08b152afcfae 9 days ago 133MB

接著用自製的 php image 來執行 composer 指令

1
docker run --rm -v $(pwd):/app lnmp_php composer create-project laravel/laravel /app/take1
  • --rm:該 container 執行後就會刪除,因為建立專案是一次性的行為,因此加上自行刪除指令就不會留下多餘的 container
  • -v $(pwd):/app:將目前的資料夾掛載到 container 裡的 /app
  • lnmp_php:php 的 image 檔
  • composer create-project laravel/laravel /app/take1:用 composer 在 container 裡的 /app/take1 中建立 Laravel 專案,如果 composer 是裝在本機而非 docker image 的話,只需要這一行就夠了。
    另外因為本機的 projects 資料夾已和 container 中的 /app 做掛載綁定,因此指令完成後,就會在本機的 projects 裡建立一個名為 take1 的資料夾,裡面的內容即為 Laravel 的檔案

開啟 Laravel 的測試頁

還記得之前在 nginx conf 中設的 server_name「my-lab」 嗎?我們先給網站指定了域名,但這只是個假域名,所以記得先去 /etc/hosts 把 my-lab 這個域名與這台 Docker 主機的 ip 做 mapping。

接著就來試著用瀏覽器打開 Laravel 的首頁 http://my-lab/take1/public/ 看是否运行成功。

測試 Laravel 與 MariaDB 的連線

最後我們來測試 Laravel 是否可正常的與 DB 連線,如果只是想單純測試 php 與 DB 的連線,只要新增一段程式碼到 ./projects/take1/public 裡就可以了

1
vim ./projects/take1/public/db_test.php
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<?php
$servername = "mariadb";
$database = "laravel";
$username = "laravel_user";
$password = "abcd1234";

// Create connection
$conn = new mysqli($servername, $username, $password, $database);
// Check connection
if ($conn->connect_error) {
die("Connection failed: " . $conn->connect_error);
}
echo "Connected successfully";
?>

再來用 curl 試一下這隻程式

1
2
3
4
5
curl -L 'http://my-lab/take1/public/db_test.php'

# 執行結果

Connected successfully

不過除了 php 之外,我也想知道 Laravel 能否正常連到 DB,這時我們就先編輯一下 Laravel 的 .env 這個檔案

1
vim ./projects/take1/.env

找到下面這段,並把它改成我們的連線資訊

1
2
3
4
5
6
DB_CONNECTION=mysql
DB_HOST=mariadb
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=laravel_user
DB_PASSWORD=abcd1234

最後再以 Laravel artisan 指令的 migrate 當作測試

1
2
3
4
5
docker exec -it php php /my_projects/take1/artisan migrate:status

# 執行結果

Migration table not found.

有出現 table not found 就表示 Laravel 有連到 DB 了(不然只會出現錯誤的訊息)


參考資料:

PHP 7.4 PHP-FPM Alpine with core extensions gd

USE USERMOD AND GROUPMOD IN ALPINE LINUX DOCKER IMAGES

composer create-project, the permissions of all directories is 777?

Using a custom user for PHP-FPM and Nginx configurations in docker containers

Check for database connection, otherwise display message

转自:https://notes.wadeism.net/developer/2825/