Laravel9 Production環境容器化
本篇文章主要紀錄Laravel9、php8.1 Production環境容器化過程
PHP Production基礎環境Image
因為Laravel需要的環境比較複雜
要安裝composer、各種php extension、nginx等套件
如果這些安裝每次都在production build的時候執行會很浪費時間
因此通常我們會建立一個Laravel專用的基礎Image
先把這些東西在基礎Image裝好
以供後續在Laravel production build的時候使用
準備檔案
- Dockerfile
- nginx(folder)
- index.html
- nginx.conf
- nginx-default.conf
- supervisord.conf
- docker-compose.yml
nginx/index.html
作為nginx預設的index.html
單純用在驗證nginx是否啟動成功
可自行決定index.html要顯示怎麼樣的網頁
nginx/nginx.conf
主要拿來覆蓋container內的/etc/nginx/nginx.conf設定
依照專案可自行設定
通常我是拿來啟動gzip壓縮、nginx virtual host config include path
一開始不知道要設定什麼可先不覆蓋
啟動container後複製/etc/nginx/nginx.conf的設定再拿來客製化覆蓋
nginx/nginx-default.conf
用來覆蓋預設的nginx virtual host設定
以此版本alpine安裝的nginx virutal host設定在/etc/nginx/http.d/default.conf
主要設定內容如下
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/html;
index index.html index.htm index.nginx-debian.html;
server_name _;
location ~ .(html)$ {
add_header Cache-Control "max-age=0, no-cache, no-store, must-revalidate";
add_header Pragma "no-cache";
add_header X-Frame-Options "SAMEORIGIN";
}
location ~ (css|js|map|jpg|jpeg|png|ico|gif|woff|woff2|svg|ttf|eto|br|gz)$ {
add_header Cache-Control "max-age=86400, must-revalidate";
}
location / {
try_files $uri $uri/ =404;
}
location = /404.html {
internal;
}
}
supervisord.conf
這個檔案為supervisor設定(supervisor是一種process monitor)
在container啟動後
透過supervisor同時啟動php-fpm及nginx
[supervisord]
user=root
nodaemon=true
logfile=/dev/stdout
logfile_maxbytes=0
pidfile=/var/run/supervisord.pid
loglevel = INFO
[program:php-fpm]
command = /usr/local/sbin/php-fpm
autostart=true
autorestart=true
priority=5
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
autostart=true
autorestart=true
priority=10
stdout_events_enabled=true
stderr_events_enabled=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
Dockerfile
這邊使用的Image為php8.1-fpm-alpine
FROM php:8.1-fpm-alpine
WORKDIR /var/www/html
COPY ./supervisord.conf /etc/supervisord.conf
COPY ./nginx/nginx.conf /etc/nginx/nginx.conf
COPY ./nginx/nginx-default.conf /etc/nginx/http.d/default.conf
COPY ./nginx/index.html /var/www/html/index.html
RUN apk update \
# 常用套件安裝
&& apk add --no-cache zip unzip dos2unix supervisor libpng-dev libzip-dev freetype-dev $PHPIZE_DEPS libjpeg-turbo-dev tzdata \
# composer安裝
&& curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/local/bin --filename=composer \
&& composer --ansi --version --no-interaction \
# php extension安裝
&& docker-php-ext-install gd pcntl bcmath mysqli pdo_mysql \
# php extension設定
&& docker-php-ext-configure gd --with-freetype --with-jpeg \
# 透過pecl安裝php extension
&& pecl install zip \
&& docker-php-ext-enable zip \
&& pecl install igbinary \
&& docker-php-ext-enable igbinary \
# imagemagick
&& apk add imagemagick imagemagick-dev \
&& printf "\n" | pecl install -o -f imagick \
&& docker-php-ext-enable imagick \
&& php -m | grep imagick \
# nginx
&& apk add nginx \
&& ln -sf /dev/stdout /var/log/nginx/access.log \
&& ln -sf /dev/stderr /var/log/nginx/error.log
EXPOSE 80
# container啟動時執行supervisor
CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisord.conf"]
docker-compose.yml
這個docker-compose.yml用於本機測試用
version: '3'
services:
php8.1-fpm-nginx-alpine:
container_name: php8.1-fpm-nginx-alpine
build:
context: ./
ports:
- "9000:80"
測試Image
透過剛才的docker-compose.yml啟動測試環境
docker-compose build --no-cache; docker-compose up
啟動後應該就會看到container log出現啟動php-fpm, nginx等訊息
前往http://localhost:9000就會看到先前建立的測試index.html內容
Laravel Image
以下production image相關檔案
皆放在laravel project下集中一個目錄管理(以下範例的目錄名稱為docker-production)
好處是如果開發環境是用sail的話
不會跟laravel project第一層的docker-compose.yml混在一起
相關檔案
- nginx(folder)
- nginx-default.conf
- php-fpm(folder)
- www.conf
- supervisord
- supervisord.conf
- .dockerignore
- docker-compose.yml
- Dockerfile
nginx/nginx-default.conf
此檔案將用於覆蓋預設的nginx virtual host設定
大部分設定皆參考Laravel Deployment文件的Nginx設定段落
整個Laravel Project將會複製至container中的/var/www/app中
另外要注意的是fastcgi_pass要設定為127.0.0.1:9000
這個是php-fpm-alpine中預設的php-fpm socket port(可參考此官方php-fpm-alpine image的Dockerfile source code)
server {
listen 80 default_server;
listen [::]:80 default_server;
root /var/www/app/public;
index index.php;
server_name _;
add_header X-Frame-Options "SAMEORIGIN";
add_header Strict-Transport-Security "max-age=31536000; includeSubdomains";
add_header X-Content-Type-Options "nosniff";
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 127.0.0.1:9000;
fastcgi_param SCRIPT_FILENAME $realpath_root$fastcgi_script_name;
include fastcgi_params;
}
location ~ /\.(?!well-known).* {
deny all;
}
}
php-fpm/www.conf
依照需求自行調整
[www]
user = www-data
group = www-data
listen = 127.0.0.1:9000
listen.owner = www-data
listen.group = www-data
pm = dynamic
pm.max_children = 50
pm.start_servers = 20
pm.min_spare_servers = 10
pm.max_spare_servers = 20
supervisord/supervisord.conf
一樣設定啟動、監控php-fpm及nginx
[supervisord]
user=root
nodaemon=true
logfile=/dev/stdout
logfile_maxbytes=0
pidfile=/var/run/supervisord.pid
loglevel = INFO
[program:php-fpm]
command = /usr/local/sbin/php-fpm
autostart=true
autorestart=true
priority=5
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
[program:nginx]
command=/usr/sbin/nginx -g "daemon off;"
autostart=true
autorestart=true
priority=10
stdout_events_enabled=true
stderr_events_enabled=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
Dockerfile
這邊特別要說明的是php-fpm的設定
複製到container內的檔名為何是99-www.conf
原因是php-fpm跟php設定一樣有優先權
由數字再到字串做排序
因次設定檔名為99-www.conf將可得到最高優先權覆蓋/usr/local/etc/php-fpm.d/內的所有設定
FROM your/base/image/url:image-tag
ENV APP_PATH=/var/www/app
ENV TZ=Asia/Taipei
WORKDIR /var/www/app
COPY ./ $APP_PATH
COPY docker-production/nginx/nginx-default.conf /etc/nginx/http.d/default.conf
COPY docker-production/php-fpm/www.conf /usr/local/etc/php-fpm.d/99-www.conf
COPY docker-production/supervisord/supervisord.conf /etc/supervisord.conf
RUN composer install --optimize-autoloader --no-dev \
&& cp .env.example .env \
&& composer dump-autoload -o \
&& chown www-data:www-data -R /var/www/app/ \
&& chmod 775 -R $APP_PATH \
&& chmod o+w -R $APP_PATH/storage
&& php artisan storage:link \
&& php artisan config:cache \
&& php artisan route:cache \
&& php artisan view:cache
EXPOSE 80
CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisord.conf"]
docker-compose.yml
上面有提到這些production build相關檔案都放在laravel project底下的docker-production目錄中
因此context必須設定至上一層laravel project中
dockerfile再往下指向docker-production/Dockerfile中
Dockerfile才能正常使用COPY ./來複製整個laravel project
version: '3.8'
services:
laravel9:
container_name: laravel9
build:
context: ../
dockerfile: docker-production/Dockerfile
ports:
- "9000:80"
使用docker build command
前面有提到因為context在上層的laravel project
因此docker build路徑必須使用../才能正常執行
docker build -t [image-tag] --file Dockerfile ../
Cronjob
若要在container內執行cronjob
一樣可透過supervisor來啟動
crontab
首先在外部(docker-production目錄)建立一個crontab檔案
此範例為每分鐘執行一次/app/my-schedule.sh這個shell script檔案
* * * * * sh -c /app/my-schedule.sh
my-schedule.sh
外部一樣要建立一個my-schedule.sh檔案
這邊用一個很簡單的範例
執行的job就是echo出當下的時間
echo "cronjob now: $(date)"
Dockerfile設定
補上兩個COPY檔案
/var/spool/cron/crontabs/root就是container內的cronjob設定檔位置(同crontab -e編輯檔案)
COPY docker-production/my-schedule.sh /app/my-schedule.sh
COPY docker-production/crontab /var/spool/cron/crontabs/root
supervisord/supervisord.conf調整
補一段cron的process管理
使用/usr/sbin/crond -f指令將cronjob執行在Foreground(就是-f選項的用途)
[program:cron]
command=/usr/sbin/crond -f
user=root
autostart=true
autorestart=true
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0
測試
container啟動後
就會在log看到每分鐘都執行echo時間的log
注意事項
如果服務啟動的container為一個以上
要參考Laravel Schedule文件的Running Tasks On One Server段落
將cache driver改為file以外的driver
才不會同時執行多個cronjob
如果是使用k8s佈署的話
也可以考慮直接使用k8s的cronjob元件來實做
就不用特別在container內跑cronjob
Queue
這邊參考Laravel Queue文件的Supervisor configuration段落
supervisord/supervisord.conf調整
將supervisord.conf新增laravel queue worker process管理
這邊queue name先使用{QUEUE_NAME}做為變數
在Dockerfile中執行RUN的時候再另外取代
[program:laravel-worker]
command=php /var/www/app/artisan queue:work --queue={QUEUE_NAME} --tries=5 --sleep=5 --timeout=60
autostart=true
autorestart=true
numprocs=1
startretries=10
stdout_events_enabled=1
redirect_stderr=true
Dockerfile調整
使用sed取代{QUEUE_NAME}變數改為app
這樣的好處是supervisord.conf可重複使用
當然要直接寫死supervisord.conf內容也可以
就看佈署的人怎麼設定
ENV QUEUE_NAME=app
COPY docker-production/supervisord/supervisord.conf /etc/supervisord.conf
RUN sed -i "s|{QUEUE_NAME}|${QUEUE_NAME}|g" /etc/supervisord.conf
敏感資料
docker-compose
如果是使用docker-compose佈署production環境
可使用docker-compose的secret功能
將container外部環境準備好的laravel .env mapping至container內部的laravel .env中
要注意這邊是mapping不是一次性複製進container中
所以外部的laravel .env一旦更新
container內部的laravel .env一樣會被更新
version: '3.8'
services:
laravel9:
container_name: laravel9
build:
context: ../
dockerfile: docker-production/Dockerfile
ports:
- "9000:80"
secrets:
- source: laravel_env
target: /var/www/app/.env
mode: 775
secrets:
laravel_env:
# 含有敏感資訊laravel .env
file: /path/to/secret/laravel-env-file
k8s
若使用k8s可使用它的secret元件來設定