Open9

Kamal2でLaravelとRedis、ジョブワーカーをデプロイするまで

uruositeuruosite

設定に自信がないためScrap。削除予定:なし


目標

VPSに、自力でLaravel、Redisのジョブワーカー、cronスケジューラを立ち上げる。1台当たり月額1000円までを目安にする。

https://laravel.com/docs/12.x/horizon#installing-supervisor

VPSの利点として、共用レンタルサーバーでは常駐が難しいLaravel Horizonを使える点がある。最近のLaravelはPaaSでエコシステムを持続させる方向性なので、「設定が難しい?Cloudあるよ」みたいなドキュメントになりつつあるが、LaravelのPaaS(ForgeやCloud)は避ける。


下記の記事を参考にする。

https://world.hey.com/tonysm/deploying-laravel-apps-with-kamal-2-0-6143d288

uruositeuruosite

dotenvのgemを使うので、Kamalは直接インストールする。

gem install bundler
bundler install dotenv kamal
bundler exec kamal init

基本的にdotenvでKamalに環境変数を渡す。後述の記事でマクロにする。

bundle exec dotenv -f .env.kamal.<環境> kamal <コマンド> -d <環境> 
uruositeuruosite

Dockerfile

serversideup/phpとFrankenPHPから迷ったが、serversideup/phpのalpine (nginx+fpm) ver.を使用することにした。

Tony Messias氏のDockerfileから以下を変更

https://world.hey.com/tonysm/deploying-laravel-apps-with-kamal-2-0-6143d288

  • NODE_VERSION指定追加
  • node, npmはalpineのnodeイメージから引っ張ってくる

serversideup/phpの特筆すべき点として、Laravelのコマンドを勝手に実行してくれるスクリプトがついている。

https://github.com/serversideup/docker-php/blob/main/src/common/etc/entrypoint.d/50-laravel-automations.sh

Dockerfile
# プロダクション用のDockerfile (サイズ目安:350MB)
# Original Author: Tony Messias
# https://world.hey.com/tonysm/deploying-laravel-apps-with-kamal-2-0-6143d288
ARG NODE_VERSION=22
FROM node:${NODE_VERSION}-alpine AS node
# ====================================================================
FROM ghcr.io/serversideup/php:8.4-fpm-nginx-alpine AS base

# Laravelのキャッシュ系コマンドを自動で発火する
# https://serversideup.net/open-source/docker-php/docs/reference/environment-variable-specification
ENV AUTORUN_ENABLED=true

ENV PHP_OPCACHE_ENABLE=1

USER root

RUN apk add --update busybox-suid && \
    install-php-extensions bcmath gd exif

COPY --from=composer /usr/bin/composer /usr/bin/composer

USER www-data

# ====================================================================
FROM base AS dependencies

WORKDIR /var/www/html/

COPY composer.json composer.lock /var/www/html/

# ディレクトリを作ってautoloadの警告を抑制しつつインストール
RUN mkdir -p app && \
    mkdir -p database/{factories,seeders} && \
    composer install --no-interaction --prefer-dist --no-scripts

USER root

COPY ./package.json ./vite.config.js ./package-lock.json /var/www/html/
COPY ./public/ /var/www/html/public
COPY ./resources/ /var/www/html/resources

COPY --from=node /usr/lib /usr/lib
COPY --from=node /usr/local/lib /usr/local/lib
COPY --from=node /usr/local/include /usr/local/include
COPY --from=node /usr/local/bin /usr/local/bin

RUN npm ci && npm run build

# ====================================================================
FROM base
# ソースコード
COPY --chown=www-data:www-data . /var/www/html
# vendor
COPY --from=dependencies --chown=www-data:www-data /var/www/html/vendor /var/www/html/vendor
# ビルドアーティファクト (config/deploy.ymlのasset_pathに指定する)
COPY --from=dependencies --chown=www-data:www-data /var/www/html/public/build /var/www/html/public/build

# スクリプトとoptimize-autoloaderつきで改めてインストール
RUN composer install --no-interaction --prefer-dist --optimize-autoloader

USER root

RUN rm -rf /usr/bin/composer

USER www-data

uruositeuruosite

ローカル

redis, mailpit付きのsail環境を立ち上げる

kamal-laravel-example とする。

curl -s "https://laravel.build/kamal-laravel-example?with=mailpit,redis&php=84" | bash

ローカル用Dokerfile

touch Dockerfile.local dockerfile-local.start.sh
chmod +x dockerfile-local.start.sh

以下、プロダクション用Dockerfileにsailを使うためだけのユーザーをつけてしまっている

Dockerfile.local
FROM ghcr.io/serversideup/php:8.4-cli-alpine

# Laravelのキャッシュ系コマンドを自動で発火する
# https://serversideup.net/open-source/docker-php/docs/reference/environment-variable-specification
ENV AUTORUN_ENABLED=true
# マイグレーションはしない
ENV AUTORUN_LARAVEL_MIGRATION=false
# ローカルでOPcache使うなよ!
ENV PHP_OPCACHE_ENABLE=0

ARG WWWUSER=1000
ARG WWWGROUP=1000

USER root

# serversideup/phpでsailユーザーを使えるようにする
# https://github.com/serversideup/docker-php/discussions/262
RUN usermod -ou $WWWUSER www-data \
    && groupmod -og $WWWGROUP www-data \
    && useradd -mNo -g www-data -u $(id -u www-data) sail

# あとはtonysm氏のイメージを参考に構築、ただしローカルなのでステージは分けない
# https://world.hey.com/tonysm/deploying-laravel-apps-with-kamal-2-0-6143d288
RUN apk add --update busybox-suid && \
    install-php-extensions bcmath gd exif \
    # ローカルなので追加
    pcov xdebug

RUN echo "xdebug.mode=debug" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
    && echo "xdebug.client_host=host.docker.internal" >> /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini \
    && echo "pcov.enabled=1" >> /usr/local/etc/php/conf.d/docker-php-ext-pcov.ini

EXPOSE 9003

WORKDIR /var/www/html/
# これはserversideup/phpに最初からあるステージ
COPY --from=composer /usr/bin/composer /usr/bin/composer

COPY --chown=www-data:www-data . .
RUN composer install --no-interaction --prefer-dist

# 最後にwww-dataに戻すことを忘れずに
USER www-data

COPY --chown=www-data:www-data ./dockerfile-local.start.sh /usr/local/bin/start
RUN chmod 0755 /usr/local/bin/start

CMD ["/usr/local/bin/start"]

ran氏のスクリプトを使用し、cron、horizonの実行にも対応。cronはぴったり0秒になるようsleepを調整。

https://zenn.dev/ransakata/articles/972271ebd76fe3

dockerfile-local.start.sh
#!/bin/sh
set -e
# Kamalのcronの挙動をスクリプトで再現する
# Original Author: ran
# https://zenn.dev/ransakata/articles/972271ebd76fe3

role=${CONTAINER_ROLE:-web}

if [ "$role" = "web" ]; then
    php artisan serve --host=0.0.0.0 --port=8080

elif [ "$role" = "horizon" ]; then
    php artisan horizon

elif [ "$role" = "cron" ]; then
    while true; do
        php artisan schedule:run --verbose --no-interaction

        # 2回目以降の実行が「0秒」になるよう計算
        now=$(date +%s)
        past=$(( now % 60 ))
        sleep $(( 60 - past ))
    done

else
    exit 1
fi

docker-compose

sailのデフォルトから以下を変更

  • Dockerfile.localに変更
  • cron, horizonに CONTAINER_ROLE を与えて挙動を分岐
  • DBのデプロイをKamalでする自信がないため、削除
    • accessoriesとしてデプロイすれば使えるが、デプロイ後の接続が面倒
    • よって、ローカルでsupabase CLIを使ってDBを立ち上げることにした。
    • supabaseのセットアップ云々は割愛。
  • KVストアにはRedisとクライアント互換性のあるValkeyを使用。
    • パスワードを指定する必要性は薄いが、デプロイ時にも同じ方法で設定しているので、動作確認のため設定している
docker-compose.yml
services:
    laravel.test: # sail CLIの仕様でこれを変えてはいけない
        container_name: app
        build:
            context: '.'
            # serversideup/phpを使用するよう変更
            dockerfile: Dockerfile.local
        extra_hosts:
            - 'host.docker.internal:host-gateway'
        ports:
            - '${APP_PORT:-80}:8080'
            - '${VITE_PORT:-5173}:${VITE_PORT:-5173}'
        environment:
            LARAVEL_SAIL: 1
            IGNITION_LOCAL_SITES_PATH: '${PWD}'
        volumes:
            - '.:/var/www/html'
        networks:
            - sail
        depends_on:
            - redis
            - mailpit
    # メモリ節約
    # cron:
    #     build:
    #         context: '.'
    #         dockerfile: Dockerfile.local
    #     environment:
    #         CONTAINER_ROLE: cron
    #     extra_hosts:
    #         - 'host.docker.internal:host-gateway'
    #     volumes:
    #         - '.:/var/www/html'
    #     networks:
    #         - sail
    #     depends_on:
    #         - laravel.test
    #     healthcheck:
    #         test: ["CMD", "healthcheck-schedule"]
    horizon:
        build:
            context: '.'
            dockerfile: Dockerfile.local
        environment:
            CONTAINER_ROLE: horizon
        extra_hosts:
            - 'host.docker.internal:host-gateway'
        volumes:
            - '.:/var/www/html'
        networks:
            - sail
        depends_on:
            - laravel.test
        healthcheck:
            test: ["CMD", "healthcheck-horizon"]
            start_period: 10s

    # DBコンテナはsupabaseとかで代替してください
 
    redis:
        # ホスト名はredisのままvalkeyを使用
        image: 'valkey/valkey:8'
        restart: always
        environment:
            - REDIS_PASSWORD: ${REDIS_PASSWORD}
        command: /bin/sh -c "valkey-server --requirepass $$REDIS_PASSWORD"
        ports:
            - '${FORWARD_REDIS_PORT:-6379}:6379'
        volumes:
            - 'sail-redis:/data'
        networks:
            - sail
        healthcheck:
            test: ["CMD", "valkey-cli", "ping"]
            retries: 3
            timeout: 5s
    mailpit:
        image: 'axllent/mailpit:latest'
        ports:
            - '${FORWARD_MAILPIT_PORT:-1025}:1025'
            - '${FORWARD_MAILPIT_DASHBOARD_PORT:-8025}:8025'
        networks:
            - sail
networks:
    sail:
        driver: bridge
volumes:
    sail-redis:
        driver: local

uruositeuruosite

deploy.ymlの設定がだるすぎる

secretが分かりづらく、めちゃくちゃ時間を浪費した。

下記の記事のように設定する。

https://zenn.dev/uruosite/articles/ab3695881d0871

config/deploy.yml
# Kamal2でLaravelをデプロイするための設定
# https://world.hey.com/tonysm/deploying-laravel-apps-with-kamal-2-0-6143d288
service: kamal-laravel-example
require_destination: true

<% require 'dotenv'; Dotenv.load(".kamal/secrets-common") %>
<% require 'dotenv'; Dotenv.load(".kamal/secrets.#{ENV['KAMAL_DESTINATION']}") %>

image: <%= ENV['KAMAL_REGISTRY_USERNAME'] %>/app

servers:
  web:
    hosts:
      - <%= ENV['KAMAL_SERVER_IP_ADDRESS'] %>
    env:
      # 開始時に `php artisan migrate --force --isolated` を実行
      AUTORUN_LARAVEL_MIGRATION: "true"
      AUTORUN_LARAVEL_MIGRATION_ISOLATION: "true"
      CONTAINER_ROLE: "web"
  cron:
    hosts:
      - <%= ENV['KAMAL_SERVER_IP_ADDRESS'] %>
    cmd: php artisan schedule:work
    options:
      health-cmd: healthcheck-schedule
    env:
      AUTORUN_LARAVEL_MIGRATION: "false"
      CONTAINER_ROLE: "cron"
  horizon:
    hosts:
      - <%= ENV['KAMAL_SERVER_IP_ADDRESS'] %>
    cmd: php artisan horizon
    options:
      health-cmd: healthcheck-horizon
    env:
      AUTORUN_LARAVEL_MIGRATION: "false"
      CONTAINER_ROLE: "horizon"

proxy:
  ssl: true
  host: '環境ごとに上書き'
  app_port: 8080

registry:
  server: <%= ENV['KAMAL_REGISTRY_SERVER'] %>
  username: <%= ENV['KAMAL_REGISTRY_USERNAME'] %>

  password:
    - KAMAL_REGISTRY_PASSWORD

builder:
  arch: amd64
  cache:
    type: gha

env:
  clear:
    APP_ENV: '環境ごとに上書き'
    APP_DEBUG: false
    APP_URL: '環境ごとに上書き'
    PHP_CLI_SERVER_WORKERS: 4
    BCRYPT_ROUNDS: 12
    LOG_LEVEL: '環境ごとに上書き'
    REDIS_HOST: kamal-laravel-example-redis
    VITE_APP_NAME: "kamal-laravel-example"
  secret:
    - APP_KEY
    - DB_URL
    - REDIS_PASSWORD

aliases:
  shell: app exec --interactive --reuse "sh"
  tinker: app exec --interactive --reuse "php artisan tinker"

asset_path: /var/www/html/public/build

accessories:
  redis: # kamalの仕様で、ホスト名は「<サービス名>-redis」になる
    image: redis/redis-stack-server:7.4.0-v6
    host: <%= ENV['KAMAL_SERVER_IP_ADDRESS'] %>
    port: 6379
    env:
      secret:
        - REDIS_ARGS
    directories:
      - data:/data
config/deploy.production.yml
# production環境の上書き部分のみ記述
proxy:
  host: <本番ドメイン>

env:
  clear:
    APP_ENV: production
    APP_URL: <本番URL>
    LOG_LEVEL: error
config/deploy.staging.yml
# staging環境の上書き部分のみ記述
proxy:
  host: <ステージングドメイン>

env:
  clear:
    APP_ENV: staging
    APP_URL: <ステージングURL>
    LOG_LEVEL: warning
uruositeuruosite

25/8/11追記

Redisのパスワードをシークレットとして渡したいが、redis/redis-stack-serverにはREDIS_ARGSとして渡す必要がある。そのため、secrets-commonファイルではあらかじめ展開してある。

.kamal/secrets-common
REDIS_PASSWORD=$REDIS_PASSWORD
REDIS_ARGS="--requirepass $REDIS_PASSWORD"
uruositeuruosite

デプロイ

Makefile

Makefile
define KAMAL_CMD
	@echo "→ running kamal on '$(1)' : $(2)"
	bundle exec dotenv -f .env.kamal.$(1) kamal $(2) -d $(1)
endef

# 初回デプロイ
kamal-%-first-deploy:
	$(call KAMAL_CMD,$*,setup)
# 2回目以降のデプロイ
kamal-%-redeploy:
	$(call KAMAL_CMD,$*,redeploy --verbose)

SSHでrootが必要な部分があり、仕方なくkamalにrootを使わせている... VPSは完全にkamal専用になるな。

これでweb, cron, workerが起動する。

serversideup/phpの力でマイグレーションが自動で走る。

なんか環境変数が増える

config/app.php
    // 追加 以下はデプロイ時に明示していないが、自動で設定される
    'version' => env('KAMAL_VERSION'),
    'container_name' => env('KAMAL_CONTAINER_NAME'),
uruositeuruosite

Redisを使ったジョブワーカーの動作確認

laravel horizonをインストールして、適当なジョブを追加する。

また、キューのコネクションをRedisに変更する。

環境変数は面倒なので、デフォルト値を変ええればいい。ローカルにもRedisあるし。

sail artisan make:job ジョブ
sail artisan make:command ジョブディスパッチするコマンド

デプロイしたら、2窓でこういう風に監視すれば動作確認できる。

bundle exec dotenv -f .env.kamal.staging \
    kamal app exec -d staging -r web \
       --interactive --reuse "php artisan app:ジョブディスパッチするコマンド"
bundle exec dotenv -f .env.kamal.staging \
    kamal app exec -d staging -r horizon \
        --interactive --reuse "php artisan pail -vv"

ポストデプロイフック

例えばデプロイ通知とか?

touch .kamal/hooks/post-deploy
chmod +x .kamal/hooks/post-deploy

マイグレーションはDockerfileでやってしまっている。

.kamal/hooks/post-deploy
#!/bin/bash

kamal app exec -d $KAMAL_DESTINATION -r web --reuse "php artisan app:send-deploy-notification"

exit 0
uruositeuruosite

モニタリング

さくらのVPSで2core、RAM1GB。cron/horizonのデプロイ後に激重になった。

Kamal proxy含むコンテナの動作に対し、全然足りていない

リソース情報で200%を超えているグラフ