Laravel9 Production環境容器化

2022/11/03

本篇文章主要紀錄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內容

laravel9-production-base-image-test.png

 

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

cronjob-test.png

 

注意事項

如果服務啟動的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元件來設定