ネットアシスト開発チームの yu-kinjo です。
本記事は、フルスクラッチで作成したWebアプリをDockerイメージ化して、さくらのクラウドのDockerコンテナ実行サービスである AppRun で実行してみようという趣旨の連載の第4回です。
第3回からは「AppRun専有型」を用いて複数のWebUIコンテナ間でセッションを維持する方法を模索していきます。今回は、前回作成したセッション維持用Redisコンテナと連携するようにWebアプリを作成し、Dockerイメージ化していきます。
作成したWebアプリをDockerイメージ化してAppRunで動かしてみよう(第3回) ~セッション維持編~
セッションをRedisに保存するPHP Webアプリの作成概要
今回もまたフルスクラッチでWebアプリを作成していきます。RDBとの連携は第1回~第2回で行いましたので、今回はDBの読み込みを省いた、複数WebUIコンテナ間でのセッション維持実現を主眼としたアプリを作成します。
- Webアプリ本体はPHP 8.4で実装します。
- TOPページに現在のセッション情報を表示します。
- Webアプリケーションフレームワークを使わず、以下の2ファイルで完結する非常にシンプルな構成にしています。
- index.php (TOPページ)
- session.php (セッション管理ファイル。上記ファイルから require される。)
また、コンテナやミドルウェアも含めた構成は以下のようになります。
- Webサーバーとして Nginx、PHPの実行方法として PHP-FPM の組み合わせで動作させます。どちらも起動状態を維持するために supervisor を利用します(supervisor の詳細は第1回を参照)
- Redisサーバーは第3回で構築したRedisコンテナを利用するようにします。
- Redis接続情報はコンテナ内に含めず、コンテナ起動時にパラメータとして渡せるようにします。
AppRun専有型用のコンテナ作成時注意点
第1回~第2回では Apache + php-fpm を supervisor で起動するような構成になっていましたが、今回Webサーバーには Nginx を利用するようにしています。こちらはAppRun専有型の制限事項に合わせるためにNginxの方が都合がいいためそうなっています。 具体的には以下のような制限事項が有ります。
コンテナの実行ユーザと実行グループ
uid951:gid951 で実行します。
上記のように、AppRun専有型ではroot権限を持たない uid 951 のユーザーでプロセスが実行されるようになっています。この際、Apacheでは非root権限で動作させるために調整するべき箇所が多く対処しづらく、比較的対処しやすいNginxを利用するようにしています。
また、同様の理由で、特権ポートである HTTP: 80 をコンテナで直接公開するのではなく、非特権ポートである HTTP:8080 等で公開するようにしておく必要が有ります。
PHP Webアプリ部分の実装
index.php
セッション用のCSS読み込みと、セッション情報を表示するだけのシンプルな作りです。
<?php
require __DIR__ . '/session.php';
?>
<!DOCTYPE html>
<html lang="ja">
<head>
<meta charset="UTF-8">
<title>記事一覧</title>
<style>
body { font-family: sans-serif; max-width: 800px; margin: 2rem auto; padding: 0 1rem; }
h1 { border-bottom: 2px solid #333; padding-bottom: .5rem; }
<?= session_panel_css() ?>
</style>
</head>
<body>
<h1>セッションの維持</h1>
<?= render_session_panel() ?>
</body>
</html>session.php
セッションの管理を行うファイルです。アクセスするたびにアクセスカウンタをプラスし、セッションが正常に維持されていればカウンタが増えていくようになっています。
Redisの接続情報は環境変数から読み込むようにし、コンテナデプロイ時に注入できるようにしています。
<?php
/**
* 外部Redisを使ったセッション共有の設定。
*
* Web側を複数コンテナで動かしても同じRedisにセッションを保存するため、
* どのコンテナにリクエストが振り分けられてもセッションが維持される。
*
* 接続先は環境変数で指定する(docker-compose.yml参照)。
* REDIS_HOST : Redisのホスト名(既定: redis)
* REDIS_PORT : Redisのポート (既定: 6379)
* REDIS_PASS : Redisのパスワード(任意)
*/
$redisHost = getenv('REDIS_HOST') ?: 'redis';
$redisPort = getenv('REDIS_PORT') ?: '6379';
$redisPass = getenv('REDIS_PASS') ?: '';
// php-redisのセッションハンドラ用のsave_path(tcp://host:port?auth=pass)
$savePath = sprintf('tcp://%s:%s', $redisHost, $redisPort);
if ($redisPass !== '') {
$savePath .= '?auth=' . urlencode($redisPass);
}
ini_set('session.save_handler', 'redis');
ini_set('session.save_path', $savePath);
session_set_cookie_params([
'lifetime' => 0,
'path' => '/',
'httponly' => true,
]);
session_start();
// --- セッション維持を可視化するためのカウンタ等を更新 ---
if (!isset($_SESSION['first_access'])) {
$_SESSION['first_access'] = date('Y-m-d H:i:s');
$_SESSION['views'] = 0;
}
$_SESSION['views']++;
$_SESSION['last_access'] = date('Y-m-d H:i:s');
/**
* セッションが維持されていることを示すパネルのHTMLを返す。
* 複数コンテナ構成では「処理コンテナ」が切り替わっても閲覧回数が
* 増え続けることで、Redis経由でセッションが共有されていると分かる。
*/
function render_session_panel(): string
{
$rows = [
'セッションID' => session_id(),
'初回アクセス' => $_SESSION['first_access'] ?? '-',
'直近アクセス' => $_SESSION['last_access'] ?? '-',
'閲覧回数' => (string)($_SESSION['views'] ?? 0),
'処理コンテナ' => gethostname(),
];
$html = '<div class="session-panel">';
$html .= '<h2>セッション状態(Redisで共有)</h2>';
$html .= '<table>';
foreach ($rows as $label => $value) {
$html .= '<tr><th>' . htmlspecialchars($label) . '</th>'
. '<td>' . htmlspecialchars($value) . '</td></tr>';
}
$html .= '</table>';
$html .= '<p class="hint">ページを再読み込みすると「閲覧回数」が増加します。'
. '「処理コンテナ」が切り替わっても回数が維持されれば、'
. 'Redis経由でセッションが共有されています。</p>';
$html .= '</div>';
return $html;
}
/**
* セッションパネル用のCSS(各ページのstyleに追記する)。
*/
function session_panel_css(): string
{
return <<<CSS
.session-panel { background:#f5f8ff; border:1px solid #c7d6f5; border-radius:8px;
padding:1rem 1.2rem; margin-bottom:1.5rem; }
.session-panel h2 { margin:.2rem 0 .8rem; font-size:1rem; color:#234; }
.session-panel table { border-collapse:collapse; width:100%; font-size:.9rem; }
.session-panel th, .session-panel td { text-align:left; padding:.3rem .5rem;
border-bottom:1px solid #dde6f7; }
.session-panel th { width:9rem; color:#456; font-weight:600; }
.session-panel .hint { font-size:.8rem; color:#678; margin:.8rem 0 0; line-height:1.6; }
CSS;
}
Dockerfileの設定
コンテナイメージをビルドするための設定ファイルである「Dockerfile」を作成していきます。PHP公式のイメージである「php:8.4-fpm」を元に、必要な肉付けをしていきますが、第1回~第2回とはいくつかの点で差異が有ります。
- 先述のように、第1回~第2回で利用したApacheではなく、Nginxを利用しています。
- redis接続のために必要なモジュールを pecl install するように設定しています(代わりにDB接続に必要なモジュールは粉機利用しないため取り除きました)
Dockerfile
FROM php:8.4-fpm
# nginx + supervisor を導入
RUN apt-get update && apt-get install -y \
nginx \
supervisor \
libpq-dev \
libonig-dev \
&& rm -rf /var/lib/apt/lists/*
# PHPの拡張を有効化
RUN docker-php-ext-install mbstring opcache
# Redis拡張をPECLで導入(外部Redisを使ったセッション共有のため)
RUN pecl install redis \
&& docker-php-ext-enable redis
# nginxの設定、PHP-FPMの設定、PHPの設定、supervisordの設定をコピーする
# (nginxはメイン設定ごと差し替え。8080 listen・非root対応の内容を含む)
COPY conf/nginx/nginx.conf /etc/nginx/nginx.conf
COPY conf/php-fpm/www.conf /usr/local/etc/php-fpm.d/www.conf
COPY conf/supervisord.conf /etc/supervisor/conf.d/app.conf
COPY conf/php/php.ini /usr/local/etc/php/conf.d/app.ini
# Webアプリケーション本体をコピーする
COPY src /var/www/html/
RUN chown -R www-data:www-data /var/www/html
EXPOSE 8080
CMD ["/usr/bin/supervisord", "-n", "-c", "/etc/supervisor/conf.d/app.conf"]
conf/nginx/nginx.conf
イメージビルド時にコピーされるNginxの設定ファイルです。非特権ポートである HTTP:8080 を公開するようにしておきます。
# Webコンテナ内のnginx設定。
# AppRun専有型は uid951:gid951 の非rootで実行されるため、
# pid・一時ファイル・ログはすべて非rootでも書き込める場所へ向ける。
# (user ディレクティブは非root起動では無視されるため記載しない)
worker_processes auto;
pid /tmp/nginx.pid;
error_log /dev/stderr warn;
events {
worker_connections 1024;
}
http {
include /etc/nginx/mime.types;
default_type application/octet-stream;
access_log /dev/stdout;
# 一時ファイル領域を /tmp 配下へ(非rootでも書き込み可能にする)
client_body_temp_path /tmp/nginx-client-body;
proxy_temp_path /tmp/nginx-proxy;
fastcgi_temp_path /tmp/nginx-fastcgi;
uwsgi_temp_path /tmp/nginx-uwsgi;
scgi_temp_path /tmp/nginx-scgi;
sendfile on;
keepalive_timeout 65;
server {
# デプロイ先がhttp 8080をオープンする仕様のため8080でlisten
listen 8080;
server_name _;
root /var/www/html;
index index.php;
location / {
try_files $uri $uri/ /index.php?$query_string;
}
# PHPはphp-fpm(127.0.0.1:9000)へ受け渡す
location ~ \.php$ {
fastcgi_pass 127.0.0.1:9000;
fastcgi_index index.php;
include fastcgi_params;
fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;
}
}
}conf/php/php.ini
PHPの設定ファイルです。
[PHP]
date.timezone = Asia/Tokyoconf/php-fpm/www.conf
php-fpmの設定です。
[www]
user = www-data
group = www-data
listen = 127.0.0.1:9000
pm = dynamic
pm.max_children = 5
pm.start_servers = 2
pm.min_spare_servers = 1
pm.max_spare_servers = 3conf/supervisord.conf
supervisorの設定ファイルです。ApacheではなくNginxを起動するように調整しています。
[supervisord]
nodaemon=true
logfile=/dev/null
logfile_maxbytes=0
[program:php-fpm]
command=/usr/local/sbin/php-fpm -F
autostart=true
autorestart=true
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
stdout_logfile=/dev/stdout
stdout_logfile_maxbytes=0
stderr_logfile=/dev/stderr
stderr_logfile_maxbytes=0Dockerイメージのビルド
ではDockerコンテナのイメージをビルドしていきます。
dockerコマンドでイメージのビルドを実行します。-tオプションで指定しているのはイメージ名です。第1回~第2回とは別のイメージ名にしています。
docker build --platform linux/amd64 -t sakura_app_run_session .Dockerイメージをコンテナレジストリにプッシュ
Dockerイメージがビルド出来たら、さくらのクラウドのコンテナレジストリにプッシュしていきます。
コンテナレジストリの作成方法は 第2回の記事 をご参照ください。
# コンテナレジストリにログイン
docker login <コンテナレジストリのパス>
# イメージにタグ付け
docker tag sakura_app_run_session <コンテナレジストリのパス>/sakura_app_run_session:lates
# イメージのプッシュ
docker push <コンテナレジストリのパス>/sakura_app_run_session:latestこれでAppRun専有型でWebUIを起動するためのイメージが用意出来ました。
参考: ローカルでの検証用 docker-compose
ローカルのDocker Desktop環境でもRedisコンテナと複数のWebUIコンテナ、ロードバランサ用コンテナを起動して動作検証ができます。詳細は割愛しますが、そのための docker-compose.yml ファイルをご紹介します。
docker-compose.yml
services:
# 外部Redis(セッション共有用)
redis:
image: redis:7-alpine
command: ["redis-server", "--requirepass", "redispass"]
# 検証用のため永続化は無効(最小構成)
# Webコンテナ1台目
app1:
build: ..
image: sakura_app_run_session
environment:
REDIS_HOST: redis # セッション保存先Redis
REDIS_PORT: 6379
REDIS_PASS: redispass
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
- redis
# Webコンテナ2台目(同一イメージ。複数コンテナでのセッション維持検証用)
app2:
build: ..
image: sakura_app_run_session
environment:
REDIS_HOST: redis
REDIS_PORT: 6379
REDIS_PASS: redispass
extra_hosts:
- "host.docker.internal:host-gateway"
depends_on:
- redis
# ロードバランサ(app1/app2へラウンドロビン)。ブラウザからはこのポートへアクセスする。
lb:
image: nginx:alpine
ports:
- "8080:80"
volumes:
- ./conf/nginx/lb.conf:/etc/nginx/conf.d/default.conf:ro
depends_on:
- app1
- app2conf/nginx/lb.conf
ロードバランサとして起動するNginx用の設定ファイルです。
# 検証用ロードバランサ。
# 複数のWebコンテナ(app1, app2)へ順番(ラウンドロビン)にリクエストを振り分ける。
# AppRun本番では基盤側が負荷分散するため、これはローカル検証用の代替。
upstream web_backend {
server app1:8080;
server app2:8080;
}
server {
listen 80;
location / {
proxy_pass http://web_backend;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}全体像

docker-compose 起動
起動。イメージのビルドは済ませておく事。
docker compose up -d起動後はWebブラウザから http://localhost:8080/ にアクセスして確認します。
停止
docker compose stop次回へ続く
第3回ではセッション維持用のRedisコンテナの実行、第4回となる今回は、セッション維持用Redisで実際にセッションを維持するWebアプリを実装し、Dockerイメージ化しました。
次回は今回作成したWebUI部分をAppRunで起動します。また次回、お会いしましょう。