Docker 上の Raspberry Pi OS に LAMP環境 を構築する

経緯

Raspberry Pi で LAMP環境で動作する簡単なWebアプリケーションを動かしたかったのですが、ここ数年レベルで Raspberry Pi から離れていたのでバージョン等を追いかけなければいけません。

そこで、実機の前に一度現時点で最新の Raspberry Pi OS で LAMP環境 を構築する方法を固めるために Docker で作ってみることにしました。

成果物

早速ですが成果物を。

今回の Docker Compose のベースは同じ Debian系 ということで、Docker に Ubuntu + PHP8.1 + Xdebug + SQLite 環境を構築してみるで作成した Gharapuri を土台にして作りました。

compose.yml

version: '3.9'
services:
  lampberry:
    build:
      context: ./docker
      dockerfile: Dockerfile
      args:
        WEB_ROOT_DIRECTORY: $WEB_ROOT_DIRECTORY
        PHPMYADMIN_ROOT_DIRECTORY: $PHPMYADMIN_ROOT_DIRECTORY
        PHPMYADMIN_VERSION: $PHPMYADMIN_VERSION
    labels:
      lamp.lampberry.pi: "LAMP Server on Raspi as lampberry"
    volumes:
      # workspace
      - ./workspace:/workspace
      # docker settings template
      - ./template:/template
      # apache log
      - ./apache/log:/var/log/apache2
      # PHP ini
      - ./php/conf/php.ini:/etc/php/${PHP_VERSION}/fpm/php.ini
      # PHP conf
      - ./php/conf/php-fpm.conf:/etc/php/${PHP_VERSION}/fpm/php-fpm.conf
      # PHP error log
      - ./php/error_log:/var/log/php
      # SSL
      - ./cert:/etc/ssl/private
      # MariaDB error log
      - ./mysql/log:/var/log/mysql
      # sessions
      - ./phpmyadmin/sessions:/sessions
      # phpMyAdmin ini
      - ./phpmyadmin/conf/phpmyadmin-misc.ini:/usr/local/etc/php/conf.d/phpmyadmin-misc.ini
    tty: true
    ports:
      - "$WEB_HOST_PORTNUM:$WEB_CONTAINER_PORTNUM"
      - "$WEB_HOST_PORTSSL:$WEB_CONTAINER_PORTSSL"
      - "$WEB_HOST_PORTSSH:$WEB_CONTAINER_PORTSSH"
      - "$DB_HOST_PORTNUM:$DB_CONTAINER_PORTNUM"
      - "$PHPMYADMIN_HOST_PORTNUM:$PHPMYADMIN_CONTAINER_PORTNUM"
    entrypoint: bash -c "bash /workspace/entrypoint.sh $WEB_ROOT_DIRECTORY $WEB_DOMAIN $WEB_HOST_PORTNUM $WEB_CONTAINER_PORTNUM $WEB_HOST_PORTSSL $WEB_CONTAINER_PORTSSL $WEB_SSH_USER $WEB_SSH_PASSWORD $PHP_VERSION $DB_CONTAINER_PORTNUM $DB_MARIADB_PASSWORD $PHPMYADMIN_ROOT_DIRECTORY $PHPMYADMIN_CONTAINER_PORTNUM $PHPMYADMIN_USER_NAME $PHPMYADMIN_USER_PASSWORD && /bin/bash"

基本は Gharapuri の設定を流用しています。マウントするディレクトリ・ファイルやホスト側に開放するポート等も同じです。

.env

# Apache
WEB_ROOT_DIRECTORY=sample_site
WEB_DOMAIN=lvh.me
WEB_HOST_PORTNUM=8080
WEB_CONTAINER_PORTNUM=80
WEB_HOST_PORTSSL=4043
WEB_CONTAINER_PORTSSL=443

# SSH
WEB_HOST_PORTSSH=2222
WEB_CONTAINER_PORTSSH=22
WEB_SSH_USER=root
WEB_SSH_PASSWORD=Password-1234

# PHPバージョン
PHP_VERSION=8.2

# MariaDB
DB_MARIADB_PASSWORD=Password-1234
DB_HOST_PORTNUM=13306
DB_CONTAINER_PORTNUM=3306

# MariaDB
PHPMYADMIN_ROOT_DIRECTORY=phpmyadmin
PHPMYADMIN_USER_NAME=admin
PHPMYADMIN_USER_PASSWORD=Password-1234
PHPMYADMIN_HOST_PORTNUM=8081
PHPMYADMIN_CONTAINER_PORTNUM=81
PHPMYADMIN_VERSION=5.2.1

必要なパラメータを一式入れています。

Dockerfile

FROM raspios-bullseye-armhf64-lite:2023-05-03
# args
ARG WEB_ROOT_DIRECTORY
ARG PHPMYADMIN_ROOT_DIRECTORY
ARG PHPMYADMIN_VERSION
# install tzdata without interactive
ENV DEBIAN_FRONTEND=noninteractive
# apt update & install
RUN apt update \
 && apt upgrade -y \
 && apt install -y sudo \
                   vim \
                   less \
                   procps \
                   # network ss (instaed of netstat)
                   iproute2 \
                   # apache
                   apache2 \
                   # zip
                   zip \
                   unzip \
                   # SSL
                   openssl \
                   # SSH
                   openssh-server \
                   # git
                   git \
                   # expect
                   expect \
                   # syslog
                   rsyslog \
                   # cleaning
                && apt clean \
                && rm -rf /var/lib/apt/lists/*
# install php
RUN wget -qO /etc/apt/trusted.gpg.d/php.gpg https://packages.sury.org/php/apt.gpg \
 && echo "deb https://packages.sury.org/php/ $(lsb_release -sc) main" | sudo tee /etc/apt/sources.list.d/php.list \
 && apt update \
 && apt upgrade -y \
 && apt install -y php \
                libapache2-mod-php \
                php-fpm \
                php-common \
                php-mbstring \
                php-xmlrpc \
                php-gd \
                php-xml \
                php-cli \
                php-zip \
                php-curl \
                php-imagick \
                # MySQL
                php-mysql \
                # Xdebug
                php-xdebug \
                # cleaning
                && apt clean \
                && rm -rf /var/lib/apt/lists/*
# 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/local/bin/composer
# PHP
RUN mkdir -p /run/php
RUN chown www-data:www-data /run/php
# php log
RUN mkdir /var/log/php
RUN chown www-data /var/log/php
RUN chmod 755 /var/log/php
# SSL
RUN mkdir -p /etc/ssl/private
# volume directory
RUN mkdir -p /template
RUN mkdir -p /var/www/${WEB_ROOT_DIRECTORY}
RUN mkdir -p /var/www/${WEB_ROOT_DIRECTORY}/web
RUN mkdir -p /workspace
# SSH
RUN mkdir -p /var/run/sshd
# MariaDB
RUN apt update \
 && apt upgrade -y \
 && apt install -y mariadb-server mariadb-client \
 && chown -R mysql:mysql /var/log/mysql \
 && chown -R mysql:mysql /var/lib/mysql/ \
 && mkdir /run/mysqld/ \
 && chown -R mysql:mysql /run/mysqld/
# phpMyAdmin volume directory
RUN mkdir -p /var/www/${PHPMYADMIN_ROOT_DIRECTORY}
RUN mkdir -p /var/www/${PHPMYADMIN_ROOT_DIRECTORY}/web
WORKDIR /var/www/${PHPMYADMIN_ROOT_DIRECTORY}/
RUN wget https://files.phpmyadmin.net/phpMyAdmin/${PHPMYADMIN_VERSION}/phpMyAdmin-${PHPMYADMIN_VERSION}-all-languages.tar.gz \
 && tar xvzf phpMyAdmin-*.tar.gz \
 && mv phpMyAdmin-*all-languages/* /var/www/${PHPMYADMIN_ROOT_DIRECTORY}/web/

OSのベースはARM環境の Raspberry Pi OS を Docker Desktop for Windows 上で動かすで作成したイメージを使用します。

そこに開発やテストで使いそうなパッケージや Apache, PHP, Xdebug, SSH を入れていきます。

Gharapuri との大きな違いは、 Gharapuri は SQLite 使用前提だったので入れていなかった MariaDB をインストールしていること。併せて、 GUI管理ツール として phpMyAdmin も入れています(実機(=本番環境)では先のテスト系や Xdebug, phpMyAdmin は入れない可能性が高いですが)。

phpMyAdmin は Docker だと公式イメージを使用する例が多いですが、今回はあくまで1つの Raspberry Pi 実機の中に入れることを想定しているのであえて役割ごとにコンテナは分けずに1つのコンテナで構築することを念頭にして手動インストールする方向で設定しました。

そのため、Webサーバは Apache でメインのWebサーバと同じリソースを使用、ただし仮想サイトは別です。同時に、ドメインも同じ想定になるので、後述しますがポート番号で振り分けることにします。

entrypoint.sh

#!/bin/bash

# gen key & certificate
## localhost
openssl req -new -newkey rsa:2048 -nodes \
        -out /etc/pki/tls/certs/localhost.csr \
        -keyout /etc/pki/tls/private/localhost.key \
        -subj "/C=/ST=/L=/O=/OU=/CN=www.example.com"
openssl x509 -days 365 -req \
        -signkey /etc/pki/tls/private/localhost.key \
        -in /etc/pki/tls/certs/localhost.csr \
        -out /etc/pki/tls/certs/localhost.crt

## .env domain
openssl req -new -newkey rsa:2048 -nodes \
        -out /etc/ssl/private/server.csr \
        -keyout /etc/ssl/private/server.key \
        -subj "/C=/ST=/L=/O=/OU=/CN=*.${2}"
openssl x509 -days 365 -req \
        -signkey /etc/ssl/private/server.key \
        -in /etc/ssl/private/server.csr \
        -out /etc/ssl/private/server.crt

# setting file replace and copy
sed -e "s/WEB_ROOT_DIRECTORY/${1}/gi" \
    -e "s/WEB_DOMAIN/${2}/gi" \
    -e "s/WEB_HOST_PORTNUM/${3}/gi" \
    -e "s/WEB_CONTAINER_PORTNUM/${4}/gi" \
    -e "s/WEB_HOST_PORTSSL/${5}/gi" \
    -e "s/WEB_CONTAINER_PORTSSL/${6}/gi" \
        /template/apache/apache_vh.conf > /etc/apache2/sites-available/${1}.conf
sed -e "s/WEB_ROOT_DIRECTORY/${1}/gi" \
    -e "s/WEB_DOMAIN/${2}/gi" \
    -e "s/WEB_HOST_PORTNUM/${3}/gi" \
    -e "s/WEB_CONTAINER_PORTNUM/${4}/gi" \
    -e "s/WEB_HOST_PORTSSL/${5}/gi" \
    -e "s/WEB_CONTAINER_PORTSSL/${6}/gi" \
        /template/apache/apache_vh_ssl.conf > /etc/apache2/sites-available/${1}_ssl.conf

cp /template/apache/ssl.conf /etc/apache2/mods-available/ssl.conf

# permission
chown -R apache:apache /var/www/${1}/web
find /var/www/${1}/web/ -type f -exec chmod 666 {} \;
find /var/www/${1}/web/ -type d -exec chmod 777 {} \;

# apache module enable
a2enmod ssl proxy_fcgi setenvif rewrite
# apache conf enable
echo ServerName www.example.com:${4} >> /etc/apache2/conf-available/example.conf
a2enconf example php${9}-fpm
# apache cirtual site enable
a2ensite ${1} ${1}_ssl
# apache service start
service apache2 start

# PHP
/usr/sbin/php-fpm${9} &

# SSH
sed -ri 's/^#PermitRootLogin prohibit-password/PermitRootLogin yes/' /etc/ssh/sshd_config
echo "${7}:${8}" | /usr/sbin/chpasswd
ssh-keygen -t rsa -N "" -f /etc/ssh/ssh_host_rsa_key <<< y
/usr/sbin/sshd -D &

# MariaDB
SOCKFILE="/run/mysqld/mysqld.sock"

sed -e "s/DB_CONTAINER_PORTNUM/${10}/gi" /template/mariadb/conf.d/mysql.cnf > /etc/mysql/conf.d/mysql.cnf
cp /template/mariadb/mariadb.conf.d/20-default-authentication-plugin.cnf /etc/mysql/mariadb.conf.d/20-default-authentication-plugin.cnf
cp /template/mariadb/mariadb.conf.d/40-pass.cnf /etc/mysql/mariadb.conf.d/40-pass.cnf
cp /template/mariadb/mariadb.conf.d/50-server.cnf /etc/mysql/mariadb.conf.d/50-server.cnf

# setting file replace and copy (for phpMyAdmin)
sed -e "s/PHPMYADMIN_ROOT_DIRECTORY/${12}/gi" \
    -e "s/WEB_DOMAIN/${2}/gi" \
    -e "s/PHPMYADMIN_CONTAINER_PORTNUM/${13}/gi" \
        /template/phpmyadmin/phpmyadmin_vh.conf > /etc/apache2/sites-available/${12}.conf
RANDHEXVAL=`openssl rand -hex 32`
sed -e "s/\$cfg\['blowfish_secret'\] = '';/\$cfg\['blowfish_secret'\] = '${RANDHEXVAL}';/gi" \
        /var/www/${12}/web/config.sample.inc.php > /var/www/${12}/web/config.inc.php

# permission
chown -R apache:apache /var/www/${12}/web

# apache cirtual site enable
a2ensite ${12}
# phpMyAdmin Port
echo Listen ${13} >> /etc/apache2/ports.conf
# apache service start
service apache2 reload

# MariaDB initialize
# MariaDB doesn't have --initialize option.
/usr/sbin/mysqld --user=mysql &
echo "mysqld initialized"
sed -e "s/DB_MARIADB_PASSWORD/${11}/gi" \
        /template/mariadb/mysql_secure_installation_script.sh > /workspace/mysql_secure_installation_script.sh
/workspace/mysql_secure_installation_script.sh &
echo "mysqld boot"

そして苦労したのがエントリポイント。

  • 前半部: Gharapuri のそのまま
  • MariaDB:
    • # MariaDB 以降の部分が大々的に手を入れた部分
    • 先述の通り Gharapuri は SQLite を使用することを前提にしていたので、 MariaDB に関する部分は新規
    • ベースは Ambergrease (RHEL系 の LAMP環境 構築用 Docker Compose) で賄おうとしましたが、 Ambergrease は MySQL を使用しているので MariaDB 用にチューニングし直し
      • 設定ファイルのテンプレートはなるべく使い回しにして変更を最小限にするように構成
      • sockファイル等の配置場所なども調整
    • phpMyAdmin 用の設定を準備し終えた後に初期化スクリプト(後述)を実行
      • 内部は mysql_secure_installation を実行しつつ、 expect で想定される質問文に対して回答を与えることで対話式を自動的に完了させることを想定していたのですが、何故かエントリポイント内に直書きするとこれが上手く動かない (途中で質問文と回答の順番が食い違ってしまい、途中で用意した回答を使い果たして最後まで完了しない) ので、止む無くテンプレートの .sh ファイルを別に用意(パスワードを .env の値依存で固定値ではなくしたかった)して、それを置換&コピーして、そのシェルスクリプトをエントリポイントから叩く、という力業で最後まで完了させることができました
  • phpMyAdmin:
    • # setting file replace and copy (for phpMyAdmin) 以降の部分
    • Dockerfile のところで先述した通り、 Apache を利用しつつ別の仮想サイトを作るために専用の Apache 仮想サイト用のテンプレートを用意して置換しつつ設置
    • ハッシュ値を生成してその値を入れつつ config.inc.php を作成
    • 念のため # permission の部分でパーミッション付与
    • echo Listen ${13} >> /etc/apache2/ports.conf でポート 81 (.env の値依存) を Apache の設定に追記して別ポートで待ち受ける

特に苦労したのが MariaDB の初期設定として mysql_secure_installation の実行の部分。

MySQL ならば対話式を使わないで起動時のログから初期パスワードを awk で抜いてその初期パスワードで MySQL にログイン&パスワード変更、ということができたのですが、 MariaDB ではその方法が使えなさそうだったので expect を使って対話式を自動化する方針に転換。

それも上述の通りエントリポイント直書きでは上手く行かなかったので別スクリプトで……という有様。

設定ファイル

PHP

PHP の設定周り。以下 MariaDB もそうですが、基本一度 Docker で手動でコンテナを建てて、インストール時のデフォルトの設定ファイルをホスト側にコピー、その設定ファイルで必要な個所を書き換える、という形でテンプレートやマウントする設定ファイルを作っています。

  • php.ini: 同名ファイルを手動構築コンテナから移植しつつ必要なパラメータを書き換え
  • php-fpm.conf: 同名ファイルを手動構築コンテナから移植

MariaDB

  • mysql.conf: base.cnf のパラメータを然るべきブロック内に移植
    • 先述の通り pid-filesocket の配置場所を調整
    • collation_serverutf8mb4_ja_0900_as_cs_ks がないので utf8mb4_bin でお茶を濁しました
    • log_timestamps も存在していないようなのでコメント
  • 50-server.cnf: 50-my-tuning.cnf のパラメータを然るべきブロック内に移植

phpMyAdmin

  • phpmyadmin-misc.ini: 同名ファイルを Ambergrease から移植

嵌まりポイント

先述した苦労ポイントのうち、特に嵌まった部分をピックアップ。

MariaDB で起動時にエラー

MariaDB を起動しようとすると

ERROR 2002 (HY000): Can’t connect to local MySQL server through socket ‘/run/mysqld/mysqld.sock’ (2)

のログが記録されて正常に起動しない状態になりました。

これらの記事を参考にソケットとプロセスのファイルの位置の設定がおかしかったので調整して解決。

MariaDB の初期設定 mysql_secure_installation の自動化

今回の環境を作る上で検証のため Docker 上のコンテナで手動で MariaDB をインストールして mysql_secure_installation の初期設定をスクリプトで自動化できないかと試した結果、以下のようになりました。

#!/bin/bash

MysqlRootPasswd={PASSWORD_PHRASE}

expect -c '
    set timeout 1;
    spawn sudo mysql_secure_installation;
    expect "Enter current password for root (enter for none): ";
    send -- "\n";
    expect "Switch to unix_socket authentication \[Y/n\] ";
    send -- "n\n";
    expect "Change the root password? \[Y/n\] ";
    send -- "'Y'\n";
    expect "New password: ";
    send -- "'"${MysqlRootPasswd}"'\n";
    expect "Re-enter new password: ";
    send -- "'"${MysqlRootPasswd}"'\n";
    expect "Remove anonymous users? \[Y/n\] ";
    send "Y\n";
    expect "Disallow root login remotely? \[Y/n\] ";
    send "Y\n";
    expect "Remove test database and access to it? \[Y/n\] ";
    send "Y\n";
    expect "Reload privilege tables now? \[Y/n\] ";
    send "Y\n";
    interact;'

これらの記事を参考に。ただ、先述の通り手動だと動くのですがエントリポイントで直書きだと動かなかったので別スクリプトに逃がしたわけですが……。

phpMyAdmin でログインできない

今回の環境を作る上で検証のため Docker 上のコンテナで手動で phpMyAdmin をインストールしてたところ、

mysqli::real_connect(): (HY000/1698): Access denied for user ‘root’@’localhost’

phpMyAdmin でログインしようとするとこのエラーが発生してしまいました。

MariaDB [(none)]> SELECT user,password,plugin, authentication_string from mysql.user;
+-------------+---------+-----------------------+---------+
+-------------+---------+-----------------------+---------+
| mariadb.sys |         | mysql_native_password |         |
| root        | invalid | mysql_native_password | invalid |
| mysql       | invalid | mysql_native_password | invalid |
+-------------+---------+-----------------------+---------+

参考記事を頼りにパスワードの状態を確認すると invalid になっていました。手動で

> sudo mysql -u root -p{PASSWORD} --connect-expired-password -e "ALTER USER 'root'@'localhost' IDENTIFIED BY '{PASSWORD}'; FLUSH PRIVILEGES;"

とすると

MariaDB [(none)]> SELECT user,password,plugin, authentication_string from mysql.user;
+-------------+-------------------------------------------+-----------------------+-------------------------------------------+
+-------------+-------------------------------------------+-----------------------+-------------------------------------------+
| mariadb.sys |                                           | mysql_native_password |                                           |
| root        | *XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX | mysql_native_password | *XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX |
| mysql       | invalid                                   | mysql_native_password | invalid                                   |
+-------------+-------------------------------------------+-----------------------+-------------------------------------------+

とハッシュ化されたパスワードが入っている感じになり、ログインできるようになりました。

そのため、この作業をするためにも今回は expect が必要だと感じました。

参考

PHP

LAMP

AH00534: apache2: Configuration error: No MPM loaded.

ERROR: FPM initialization failed

apache2: Syntax error on line 81 of /etc/apache2/apache2.conf: DefaultRuntimeDir must be a valid directory, absolute or relative to ServerRoot

これらのエラーへの対処が以下。

MariaDB

MySQL と MariaDB の設定の差分

全体的な差分。

log_timestamps について。 MariaDB にはない模様。

ERROR 2002 (HY000): Can’t connect to local MySQL server through socket ‘/run/mysqld/mysqld.sock’ (2)

MariaDB の初期設定 mysql_secure_installation の自動化について

今回使わなかったもの

phpMyAdmin

mysqli::real_connect(): (HY000/1698): Access denied for user ‘root’@’localhost’

Raspberry Pi

Linuxコマンドの実行結果を変数に代入

cp

この記事を書いた人

アルム=バンド

フロントエンド・バックエンド・サーバエンジニア。LAMPやNodeからWP、Gulpを使ってejs,Scss,JSのコーディングまで一通り。たまにRasPiで遊んだり、趣味で開発したり。