Docker 建立 Laravel 用的 LNMP 環境

雖然標題上是寫 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,裡面再分別建立四個資料夾,這邊將資料夾命名為:phpngnixmariadbprojects

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 需要許多額外的套件:

- BCMath
- Ctype
- Fileinfo
- JSON
- Mbstring
- OpenSSL
- PDO
- Tokenizer
- XML

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

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

- 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 如下:

vim ./php/Dockerfile
FROM php:7.4-fpm-alpine
# @see https://hub.docker.com/r/jpswade/php7.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\/Taipei"/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 的資料夾,再新增設定檔如下:

mkdir ./nginx/conf.d
vim ./nginx/conf.d/laravel.conf
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 都建構出來執行,內容如下:

vim docker-compose.yml
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/Taipei

  # 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 寫好後,直接執行即可

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 是否有成功的執行

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 成功啟動後,表示 php、nginx、MariaDB 應該有順利運行了,再我們就建個資料庫給 Laravel 使用


首先用 root 登入 container 裡的 DB

docker exec -it mariadb mysql -u root -p

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


建立名為 laravel 的資料庫

CREATE DATABASE laravel;


新增使用者 laravel_user,密碼為 abcd1234

CREATE USER 'laravel_user' IDENTIFIED BY 'abcd1234';


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

GRANT ALL PRIVILEGES ON laravel.* TO 'laravel_user';
FLUSH PRIVILEGES;
quit;

用 container 裡的 composer 建立 Laravel 專案

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

首先進到 projects 目錄中

cd projects


查看 php image 的名稱

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 指令

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 專案建立成功,而且 php、nginx 也正常運作中!

測試 Laravel 與 MariaDB 的連線

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

vim ./projects/take1/public/db_test.php
<?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 試一下這隻程式

curl -L 'http://my-lab/take1/public/db_test.php'
# 執行結果

Connected successfully


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

vim ./projects/take1/.env


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

DB_CONNECTION=mysql
DB_HOST=mariadb
DB_PORT=3306
DB_DATABASE=laravel
DB_USERNAME=laravel_user
DB_PASSWORD=abcd1234


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

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

wade Written by:

本站站長