WPP
podman で Caddy + CakePHP5 を動かす

前回の続きです。php-fpm コンテナに CakePHP5 をインストールできたのでいよいよ CaddyServer を入れます。

目的の構成

一旦、今回 podman で建てようとしているコンテナとポートの構成を説明しておきます。

  • CaddyServer ホストポート:8080 > コンテナポート:80
  • php-fpm ホストポート: – > コンテナポート:9000
  • MariaDB ホストポート:3306 > コンテナポート:3306
  • ./html フォルダを作り CaddyServer と php-fpm からそれぞれリンクする

手順

Caddy コンテナ

Caddyfile を作成します。 / で CakePHP をホストする場合はシンプルです。

$ mkdir conf
$ vi conf/Caddyfile
:80 {
	# Set this path to your site's directory.
	root * /usr/share/caddy/app/webroot
	encode gzip

	# Enable the static file server.
	file_server

	# Another common task is to set up a reverse proxy:
	php_fastcgi localhost:9000

	# log
}

最初 /app などのサブディレクトリでホストしようとしましたが、参考リンクによるとサブディレクトリでホストするのは難しいらしいのでそうそうにあきらめました。

$ podman run --name caddy -p 8080:80/tcp \
  -v ./conf/Caddyfile:/etc/caddy/Caddyfile \
  -v ./html:/usr/share/caddy \
  caddy:latest

ここまでで MariaDB、Caddy、php−fpm の3つのコンテナが揃いました。podman 的には kubernetes の yaml ファイルにしろってことなので簡単に生成するために、podman-desktop で pod を作ります。

そのまま pod を yaml に吐き出したものを少し修正します。修正した結果はこんな感じです。

# Save the output of this file and use kubectl create -f to import
# it into Kubernetes.
#
# Created with podman-3.4.4
apiVersion: v1
kind: Pod
metadata:
  creationTimestamp: "2024-04-29T05:49:28Z"
  labels:
    app: caddy-pod
  name: caddy-pod
spec:
  containers:
  - args:
    - mariadbd
    image: docker.io/library/mariadb:11.3
    name: maria
    ports:
    - containerPort: 3306
      hostPort: 3306
    - containerPort: 80
      hostPort: 8080
    resources: {}
    securityContext:
      capabilities:
        drop:
        - CAP_MKNOD
        - CAP_NET_RAW
        - CAP_AUDIT_WRITE
    volumeMounts:
    - mountPath: /var/lib/mysql
      name: maria-data
  - image: docker.io/library/caddy:latest
    name: caddy
    resources: {}
    securityContext:
      capabilities:
        drop:
        - CAP_MKNOD
        - CAP_NET_RAW
        - CAP_AUDIT_WRITE
    volumeMounts:
    - mountPath: /etc/caddy/Caddyfile
      name: caddy-file
    - mountPath: /usr/share/caddy
      name: html-caddy
  - args:
    - php-fpm
    image: localhost/my-php:latest
    name: php
    resources: {}
    securityContext:
      capabilities:
        drop:
        - CAP_MKNOD
        - CAP_NET_RAW
        - CAP_AUDIT_WRITE
    volumeMounts:
    - mountPath: /usr/share/caddy
      name: html-fpm
    - mountPath: /usr/local/etc/php/php-fpm.conf
      name: fpm-conf
    - mountPath: /usr/local/etc/php/php.ini
      name: php-ini
  restartPolicy: Never
  volumes:
  - hostPath:
      path: /opt/ws/podman-caddy/maria/data
      type: Directory
    name: maria-data
  - hostPath:
      path: /opt/ws/podman-caddy/conf/Caddyfile
      type: File
    name: caddy-file
  - hostPath:
      path: /opt/ws/podman-caddy/html
      type: Directory
    name: html-caddy
  - hostPath:
      path: /opt/ws/podman-caddy/html
      type: Directory
    name: html-fpm
  - hostPath:
      path: /opt/ws/podman-caddy/php-fpm/php/php-fpm.conf
      type: File
    name: fpm-conf
  - hostPath:
      path: /opt/ws/podman-caddy/php-fpm/php/php.ini-development
      type: File
    name: php-ini
status: {}

この yaml ファイルで pod を起動してみます。

$ podman play kube caddy-pod.yaml

localhost:8080 にブラウザアクセスしてみます。

MariaDB との接続がまだできていませんが、動作するところまで到達しました。

参考

The “subfolder problem”, OR, “why can’t I reverse proxy my app into a subfolder?” – Wiki – Caddy Community

podman で cakephp5を動かす

だいぶ佳境に入ってきましたが podman で cakephp5 を動かす続きです。php-fpm コンテナを作り、CakePHP5 をインストールします。CaddyServer は長くなるので次の記事にします。

目的の構成

一旦、今回 podman で建てようとしているコンテナとポートの構成を説明しておきます。

  • CaddyServer ホストポート:8080 > コンテナポート:80
  • php-fpm ホストポート:- > コンテナポート:9000
  • MariaDB ホストポート:3306 > コンテナポート:3306
  • ./html フォルダを作り CaddyServer と php-fpm からそれぞれリンクする

本来なら MariaDB のポートは、php-fpm からしかアクセスしないのでホスト側に露出しなくて良いのですが、ホスト側から触れたほうが便利なので便宜上開けてます。インターネット上のサーバに建てる際は開けないほうがいいでしょう。

Caddy と php-fpm コンテナは分けなくてもいいのかもしれませんが今回は分けています。

php-fpm の設定はちょっと面倒なのでちゃんと手順を追って示していきます。

手順

php-fpm コンテナ

php-fpm イメージのビルド

php のコンテナはイメージ持ってくるだけでは済まないので Dockerfile をつくります。

FROM php:8.3-fpm-bookworm
LABEL MAINTAINER="mnishi"
ARG UID=1000
ARG GID=1000

RUN apt-get update && \
    apt-get install -y git zip unzip vim libonig-dev libxml2-dev wget && \
    docker-php-ext-install intl pdo_mysql bcmath mbstring simplexml
RUN wget https://getcomposer.org/installer -O composer-installer.php && \
    php composer-installer.php --filename=composer --install-dir=/usr/local/bin && \
    composer self-update

RUN usermod  -u $UID www-data && \
    groupmod -g $GID www-data

WORKDIR /var/www

イメージは、Debian の方がまだ慣れているので 8.3-fpm-bookworm としました。

apt-get で必要なモジュールをインストールしてますが、vim は完全な好みなので入れなくていいです。wget も不要かもしれません。

docker-php-ext-install で CakePHP に必要なモジュールをインストールします。

/usr/local/bin に composer をインストールします。

ARG UID= とARG GID= は、ホスト側の実行ユーザーの uid、gid を渡すための環境変数です。もしかすると初期値はいらないかもしれません。

php-fpm コンテナの起動

ビルドする前に www-data ユーザをホストに作っておき、ログインユーザーのグループにも追加しておきます。

$ useradd -M www-data
$ usermod -aG www-data [ログインユーザー]

ビルドします。Dockerfile と同じフォルダで以下のコマンドを実行します。

$ buildah bud --build-arg UID=$(id -u www-data) --build-arg GID=$(id -g www-data) --tag my-php . 

一旦コンテナを起動して、設定ファイルを php-fpm フォルダに取り出します。(フォルダ名はなんでもいいです。)

$ mkdir php-fpm
$ podman run --name tmp-php -d localhost/my-php
$ podman cp tmp-php:/usr/local/etc/php/php.ini-production ./php-fpm
$ podman cp tmp-php:/usr/local/etc/php/php.ini-development ./php-fpm
$ podman cp tmp-php:/usr/local/etc/php/php-fpm.conf ./php-fpm
$ podman rm -f tmp-php

使わないかも(設定を変更しないかも)しれませんが、一応 php.ini と php-fpm.conf を取ってきます。

取ってきた設定ファイルを使用してコンテナを起動します。

podman run -d --name php \
    -v ./php-fpm/php/php-fpm.conf:/usr/local/etc/php/php-fpm.conf \
    -v ./php-fpm/php/php.ini-development:/usr/local/etc/php/php.ini \
    -v ./html:/usr/share/caddy \
    my-php

起動に失敗するようでしたら、-d を外してエラーを眺めてください。

php-fpm コンテナ内で CakePHP のインストール

ここまで全てがうまく行っていれば、コンテナの中に入って composer で CakePHP をデプロイできるはずです。

; ホスト側から
$ podman exec -it caddy-pod-php /bin/bash 

 ; コンテナの中に入った
# cd /usr/share/caddy
# composer create-project --prefer-dist cakephp/app:"5.*" app
Do not run Composer as root/super user! See https://getcomposer.org/root for details
Continue as root/super user [yes]? yes
; root  で動かしているよと怒られるが無視する

.... いろいろログが流れる

Set Folder Permissions ? (Default to Y) [Y,n]? y
Permissions set on /usr/share/caddy/app/tmp/cache
Permissions set on /usr/share/caddy/app/tmp/cache/models
Permissions set on /usr/share/caddy/app/tmp/cache/persistent
Permissions set on /usr/share/caddy/app/tmp/cache/views
Permissions set on /usr/share/caddy/app/tmp/sessions
Permissions set on /usr/share/caddy/app/tmp/tests
Permissions set on /usr/share/caddy/app/tmp
Permissions set on /usr/share/caddy/app/logs
Updated Security.salt value in config/app_local.php

流行りのルートレス構成ではないので、怒られますがひとまずそのまま行きます。(慣れた所でもう一度ルートレス構成をやってみてもいいかもしれない。)

CakePHP のインストールで php モジュールが不足してエラーが出た場合は、exec でコンテナ内に入り docker-php-ext-install で不足モジュールをインストールして composer が動作するか確認します。
うまく行ったら、Docker ファイルに不足モジュールを追加しします。その後、イメージのビルドからやり直します。

参考

podman で nginx + php + mariadb を立ち上げてみる | zu-min.com

CakePHP5 の日本語チュートリアルでハマったら英語版を見よう!

追記: プルリクが取り込まれたのでこのページの内容は本家見れば OK になりました。

引続き、CakePHP5 のチュートリアルを進行してますが、どうもコードサンプルが古いバージョンのもののようです。そのため、少しハマりました。まあ、英語版見ろよと。。。

具体的には CMS チュートリアル – Articles コントローラーの作成 – 5.x の ArticleController の paginator 部分でエラーが発生します。

英語版 を見てみるとコードが違っていました。こんな感じです。

<?php
// src/Controller/ArticlesController.php

namespace App\Controller;

class ArticlesController extends AppController
{
    public function index()
    {
        $articles = $this->paginate($this->Articles);
        $this->set(compact('articles'));
    }
}

paginate はコンポーネントではなくてそもそも組み込まれているようです。日本語版のドキュメントにプルリクだしましたが、英語力が怪しすぎるので蹴られるかもしれません。

わりとちゃんと翻訳されてそうな雰囲気だったので盲信してチュートリアル進めてきたのですが、思わぬ落とし穴に突き当たりました。せめてチュートリアルページだけでもちゃんと整備しておかないとますます CakePHP の不人気に拍車がかかりそうだなと感じた一幕でした。

CakePHP の migrations で外部キーを作る時は、unsigned にするといいかもしれない話

ある長年動いていた CakePHP 3 アプリを移行するために、一旦 CakePHP5 の tuta をやり始めたところだ。

Cake公式のチュートリアルはブログアプリをつくることになるのだが、DB の作成はベタで SQLを叩くようになっている。そのままやってももちろんいいし最短時間で目的の状態になるのだが、今回は移行時に使うことになるのであえて migrations を使ってみた。

で要点から、 migrations はデフォルトで、id フィールドを作ってくれるのですがそれが int(11) になっているようです。一方、migrations で integer でフィールドを作成すると int(10) になるのがデフォルトのようです。
となると結果として外部キーを作成するときにフィールドの桁数が合わずに作れずエラーになるという事象に遭遇したという話です。

ブログチュータで実際のコードはこんな感じになります。

ファイル名の先頭部分は日時を示します。 空のファイル自身もこんなふうに migrations で生成できます。
./app/bin/cake migrations create CreateUsers

でユーザーテーブルはこれです。

<?php
declare(strict_types=1);

use Migrations\AbstractMigration;

class CreateUsers extends AbstractMigration
{
    /**
     * Change Method.
     *
     * More information on this method is available here:
     * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
     * @return void
     */
    public function change(): void
    {
        $table = $this->table('users');
        $table->addColumn('email', 'string', [
            'default' => '',
            'limit' => 255,
            'null' => false,
            'comment' => ''
        ])
        ->addIndex(['email']);
        $table->addColumn('password', 'string', [
            'default' => '',
            'limit' => 255,
            'null' => false,
        ]);
        $table->addColumn('created', 'datetime', [
            'default' => null,
            'null' => false,
        ]);
        $table->addColumn('modified', 'datetime', [
            'default' => null,
            'null' => false,
        ]);
        $table->create();
    }
}

アーティクルテーブルはこうなりました。

user_id フィールドの signed を false にする必要がある。

<?php
declare(strict_types=1);

use Migrations\AbstractMigration;

class CreateArticles extends AbstractMigration
{
    /**
     * Change Method.
     *
     * More information on this method is available here:
     * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
     * @return void
     */
    public function change(): void
    {
        $table = $this->table('articles');
        $table->addColumn('user_id', 'integer', [
            'null' => false,
            'signed' => false,
        ]);
        // ->addIndex(['user_id']);
        $table->addColumn('title', 'string', [
            'default' => '',
            'limit' => 255,
            'null' => false,
        ]);
        $table->addColumn('slug', 'string', [
            'default' => '',
            'limit' => 191,
            'null' => false,
        ]);
        $table->addColumn('body', 'text', [
            'default' => null,
            'null' => true,
        ]);
        $table->addColumn('published', 'boolean', [
            'default' => false,
        ]);
        $table->addColumn('created', 'datetime', [
            'default' => null,
            'null' => false,
        ]);
        $table->addColumn('modified', 'datetime', [
            'default' => null,
            'null' => false,
        ]);
        $table->addIndex('slug', [
            'unique' => true
        ]);
        $table->addForeignKey('user_id', 'users', 'id',
            [
                'delete' => 'NO_ACTION', 
                'update' => 'NO_ACTION'
            ]
        );
        // $table->changePrimaryKey(['article_id', 'tag_id']);
        $table->create();
    }
}

tags テーブルはこうなる。

<?php
declare(strict_types=1);

use Migrations\AbstractMigration;

class CreateTags extends AbstractMigration
{
    /**
     * Change Method.
     *
     * More information on this method is available here:
     * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
     * @return void
     */
    public function change(): void
    {
        $table = $this->table('tags');
        $table->addColumn('title', 'string', [
            'limit' => 191,
        ]);
        $table->addColumn('created', 'datetime', [
            'default' => null,
            'null' => false,
        ]);
        $table->addColumn('modified', 'datetime', [
            'default' => null,
            'null' => false,
        ]);
        $table->addIndex('title', [
            'unique' => true
        ]);
        $table->create();
    }
}

Articles テーブルと同じように外部キーを保存するフィールドはアンサインドにする。

<?php
declare(strict_types=1);

use Migrations\AbstractMigration;

class CreateAticlesTags extends AbstractMigration
{
    /**
     * Change Method.
     *
     * More information on this method is available here:
     * https://book.cakephp.org/phinx/0/en/migrations.html#the-change-method
     * @return void
     */
    public function change(): void
    {
        $table = $this->table('aticles_tags');
        $table->addColumn('article_id', 'integer', [
            'null' => false,
            'signed' => false,
        ]);
        $table->addColumn('tag_id', 'integer', [
            'null' => false,
            'signed' => false,
        ]);
        $table->addColumn('created', 'datetime', [
            'default' => null,
            'null' => false,
        ]);
        $table->addColumn('modified', 'datetime', [
            'default' => null,
            'null' => false,
        ]);
        $table->addForeignKey('tag_id', 'tags', 'id', [
            'delete' => 'NO_ACTION', 'update' => 'NO_ACTION'
        ]);
        $table->addForeignKey('article_id', 'articles', 'id', [
            'delete' => 'NO_ACTION', 'update' => 'NO_ACTION'
        ]);
        $table->create();
    }
}

これで DBの構築はできたが、先に進んだ所で間違いがあれば修正していくかもしれない。

CakePHP で .env を使う方法

CakePHP では .env に設定値を逃して env() で読み込むことができると Cookbook: 構成設定 – 5.x に説明されている。だがしかし、説明のとおり.env.example を.env にコピーしても反映されない。(Cookbook は、.env.default となっているが実際のファイルは、.env.examlpe となっている。んー。)

検索すると Qiita にそれっぽいのがあったのでやってみるとビンゴだった。

bootstrap.php の中で .env をロードする処理がコメントアウトされているためだった。

/*
 * See https://github.com/josegonzalez/php-dotenv for API details.
 *
 * Uncomment block of code below if you want to use `.env` file during development.
 * You should copy `config/.env.example` to `config/.env` and set/modify the
 * variables as required.
 *
 * The purpose of the .env file is to emulate the presence of the environment
 * variables like they would be present in production.
 *
 * If you use .env files, be careful to not commit them to source control to avoid
 * security risks. See https://github.com/josegonzalez/php-dotenv#general-security-information
 * for more information for recommended practices.
*/
if (!env('APP_NAME') && file_exists(CONFIG . '.env')) {
    $dotenv = new \josegonzalez\Dotenv\Loader([CONFIG . '.env']);
    $dotenv->parse()
        ->putenv()
        ->toEnv()
        ->toServer();
}

今回、.env ファイルに DB 周りの設定を逃してみた。

こんな感じでホスト名やDB関連のパラメータを設定すると

export DB_HOST="maria"
export DB_ROOT="root"
export DB_ROOT_PASS="DBPASSWORD!"
export DB_NAME="CAKE_DATABASE"
export DB_USER="user"
export DB_PASS="PASSWORD"

app_local.php で env() で読み込む

return[
// 一部抜粋

  'Datasources' => [
        'default' => [
            'host' => env('DB_HOST', null),
            'username' => env('DB_USER', null),
            'password' => env('DB_PASS', null),
            'database' => env('DB_NAME', null),
            //'schema' => 'myapp',
        ],

// 抜粋終わり

CakePHP のドキュメントは割とわかりやすいので嫌いではないが、app_local.php や .env 周辺の説明は相変わらずファイル名の間違いや記述の不足があってわかりにくい。

このへんなんとかならないかな。

参考

CakePHP3で.envファイルを有効化する #PHP – Qiita

docker 内のcakephp ローカルサーバにアクセスできない時

超小ネタ。docker 内に cakephp 5 を入れてとりあえず面倒なので、Webサーバなしで軽く試そうと思い cake のローカルサーバを起動するも外部からアクセスできない現象に遭遇した。

もちろん docker -p や docker-compose.yml ports は正しく構成しています。

答えは、コンテナ内でローカルサーバを起動した画面にすでにありました。

; app に cakephp5 をインスールしています。ご自分の環境に読み替えてください。
# app/bin/cake  server 

Welcome to CakePHP v5.0.6 Console
-------------------------------------------------------------------------------
App : src
Path: /var/www/app/src/
DocumentRoot: /var/www/app/webroot
Ini Path: 
-------------------------------------------------------------------------------
built-in server is running in http://localhost:8765/
You can exit with `CTRL-C`
[Sun Mar 24 10:27:49 2024] PHP 8.3.3 Development Server (http://localhost:8765) started

cake server が localhost にバインドしているので、外からはアクセスできない状態ってことを暗に示しています。

これを動作するようにするには、0.0.0.0 にバインドします。お好みで -p でポートを変更してもいい。

# app/bin/cake server -H 0.0.0.0 

考えてみれば当たり前のことで、 Docker はホストとは別のネットワークを生成します。この時、コンテナ内の localhost へアクセスできません。 0.0.0.0 にバインドするってことは、コンテナにあるすべての NIC で待受することになるので、アクセスできるようになったってことだと思われる。