
自宅の検証用サーバー(Homelab)やクラウドのインスタンスを運用していると、ある日突然「何も書き込めなくなる」という絶望的なトラブルに遭遇することがあります😱
今回は、私の検証用ノード(k8s-worker1)で発生した「ルートディレクトリの Read-only(読み取り専用)化」という悪夢のようなファイルシステム障害の復旧から、残骸となったKubernetes(k8s)環境の完全パージ、そして最終的にDocker Composeを用いた「Prometheus + Grafana監視付きの堅牢なWordPress環境」を構築するまでの全記録を、コマンドの意味とトラブルの背景を含めて徹底的に解説します📝✨
インフラアーキテクチャやクラウド環境の運用に関わるエンジニアはもちろん、自社ブログや個人サイトをセキュアに運用したい方にとって、このトラブルシューティングと構築のプロセスは必ず役立つはずです🚀
🚨 第1章:悪夢の始まり〜ファイルシステムが「Read-only」になる日
事の発端は、不要になったコンテナやKubernetes関連のツールを削除しようとした時のことでした。コマンドを叩くと、無情にも以下のようなエラーが返ってきました。
rm: cannot remove '/usr/bin/containerd': Read-only file system
touch: cannot touch '/test_rw': Read-only file system
Linuxにおいて、ルートファイルシステム(/)が Read-only になるというのは、システムが自己防衛モード(パニック状態)に入ったことを意味します。ファイルが消せないどころか、ログの書き込みもできず、OSとしては仮死状態です👻
🔍 なぜRead-onlyになったのか?(原因の特定)
まずは一時的に書き込み権限を取り戻し、カーネルのログを確認します。
💻 Bash
# 一時的に読み書き可能(rw)で再マウント
sudo mount -o remount,rw /
# カーネルのメッセージバッファからファイルシステム(ext4)のエラーを抽出
dmesg | grep -i -E 'ext4|error|read-only|ro'
ここで出力されたログの中に、決定的な証拠がありました。[ 1.991841] EXT4-fs (sda2): orphan cleanup on readonly fs[ 1.993592] EXT4-fs (sda2): mounted filesystem ... ro with ordered data mode.
💡 【トラブルの意味】
これは、過去の強制終了やクラッシュによってファイルシステムに不整合(Dirty状態)が生じていたためです。Linuxカーネルは起動時にこれを検知すると、「これ以上データを壊さないように」と強制的にマウントオプションを ro(Read-only)に落とします。
さらに調査を進めると、システム設定にも2つの重大なミスが隠れていました。
- 📝
/etc/fstabの記述ミス
起動時のマウント設定を司るfstabのルート(/)の行が、なぜか##でコメントアウトされていました。これではOSが「どうマウントすべきか」分かりません。 - ⚙️ GRUB(ブートローダー)の強制指定
cat /proc/cmdlineでカーネルの起動引数を確認すると、ro(Read-only)が直接渡されていました。
🛠️ 復旧への道程:fsckと設定の修正
原因が分かれば、対処は明確です。ファイルシステムの不整合を直し、設定を正しい状態(rw)に書き換えます。
💻 Bash
# 1. 次回のOS起動時に強制的にファイルシステムチェック(fsck)を走らせるフラグを作成
sudo touch /forcefsck
# 2. fstabの修正(コメントアウトを外す)
sudo nano /etc/fstab
# 修正前:## /dev/disk/by-uuid/xxx / ext4 defaults 0 1
# 修正後:/dev/disk/by-uuid/xxx / ext4 defaults 0 1
# 3. GRUBの設定で ro を rw に書き換える
sudo nano /etc/default/grub
# GRUB_CMDLINE_LINUX_DEFAULT="quiet splash rw" に変更
# 4. ブートローダーの設定を更新して再起動
sudo update-grub
sudo reboot
再起動後、mount | grep " / " を実行し、見事に (rw,relatime) という表示を見たときの安堵感は、インフラエンジニアなら誰もが共感できる瞬間でしょう😮💨✨
🧹 第2章:残骸のパージ〜k8s環境の完全クリーンアップ
ファイルシステムが正常化したところで、次は中途半端に残っていた Kubernetes(kubelet, containerd 等)の残骸を完全に消し去ります🗑️
👻 systemd の罠:「masked」と「generated」
プロセスを止め、バイナリ(実行ファイル)を削除しても、systemctl list-unit-files を叩くとサービス定義が亡霊のように残っていました。
containerd.service masked
docker.service generated
💡 【コマンドの意味と背景】
- 🛑
masked(マスク状態): サービスを強制的に無効化するsystemctl maskを実行すると、設定ファイルが/dev/nullへのシンボリックリンクに置き換わります。システムは「起動不可のサービスが存在する」と認識するため、リストに残り続けます。 - 🔄
generated: systemd 以前の古い起動スクリプト(SysV initの/etc/init.d/)が残っていると、後方互換性のために自動生成サービスとして表示されます。
これらを根絶やしにするための魔法のコマンド群がこちらです🪄
💻 Bash
# マスクの解除(/dev/nullへのリンクを削除)
systemctl unmask containerd docker docker.socket 2>/dev/null
# systemd のユニットファイルを直接削除
rm -f /etc/systemd/system/containerd.service
rm -f /etc/systemd/system/docker.service
rm -f /lib/systemd/system/kubelet.service
# 旧世代のinitスクリプトを削除
rm -f /etc/init.d/docker
rm -f /etc/init.d/containerd
# systemdのデーモンを再読み込みし、孤立したキャッシュをリセット
systemctl daemon-reload
systemctl reset-failed
これでようやく which docker kubectl containerd を叩いても何も返ってこない、美しくクリーンな Ubuntu 環境を取り戻すことができました✨
🤔 なぜ 1ノードのk8s を捨てたのか?
当初はこのPC1台で k8s(k3s)を動かそうとしていましたが、最終的に破棄しました。
Kubernetesは本来、複数台のサーバーを束ねて冗長性を確保するための高度なオーケストレーションツールです。1台のノードでマスターとワーカーを兼任させると、コントロールプレーンだけで無駄なリソースを消費し、WordPressの永続化ボリューム(PV/PVC)の管理も無駄に複雑化します。「個人ブログを安定してサクッと動かす」という目的においては、Docker Compose の方が圧倒的に身軽でメンテナンス性が高いのです🐳
🐳 第3章:新生〜Docker Engineの正しいインストール
環境が綺麗になったので、公式リポジトリから最新の Docker と Docker Compose をインストールし直します。Ubuntuのデフォルトリポジトリ(apt install docker.io)はバージョンが古いことが多いため、必ず公式からGPGキーを取り込んでインストールします🔑
💻 Bash
# 必須パッケージと証明書のインストール
sudo apt-get update
sudo apt-get install -y ca-certificates curl gnupg
# 公式GPGキーの追加(なりすましパッケージを防ぐための電子署名)
sudo install -m 0755 -d /etc/apt/keyrings
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg
# リポジトリの登録
echo \
"deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" | \
sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Docker本体とプラグイン群のインストール
sudo apt-get update
sudo apt-get install -y docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin
# 起動時の自動起動を有効化
sudo systemctl enable --now docker
ここでのポイントは、旧来のPython製 docker-compose ではなく、Go言語で書き直されたプラグイン版の docker-compose-plugin(コマンドは docker compose)を導入している点です。動作が高速で、最新の v3/v4 スキーマに完全対応しています⚡
🏰 第4章:超大作・監視ツールてんこ盛りのWordPress環境構築
いよいよ本題です。ただWordPressを立てるだけでは面白くありません。脆弱性対策を施したセキュアな構成にしつつ、エンタープライズ環境顔負けのフルスタック監視ダッシュボードを1つの docker-compose.yml に詰め込みます🎁
今回盛り込むスタックは以下の通りです。
- 📝 WordPress (PHP 8.3 Apache): バージョン隠蔽化済み
- 🐬 MariaDB (10.11 LTS): 安定版データベース
- 🚢 Portainer: WebブラウザからDockerを操作できるGUI管理ツール
- 🔥 Prometheus: 時系列データを収集するメトリクスエンジン
- 📊 Grafana: 収集したデータを美しく可視化するダッシュボード
- 📈 cAdvisor: コンテナごとのCPU/メモリ消費量を監視するエージェント
- 🖥️ Node Exporter: Ubuntuホスト自体のリソース(ディスク、CPU)を監視するエージェント
🛡️ セキュリティ設定の仕込み
ディレクトリ ~/docker/wordpress を作成し、事前に設定ファイルを仕込みます。
💻 Bash
mkdir -p ~/docker/wordpress && cd ~/docker/wordpress
【PHPの脆弱性対策(php-security.ini)】
悪意のあるボットは HTTPレスポンスヘッダの X-Powered-By: PHP/8.x という情報を見て、特定の脆弱性を狙ってきます。これを隠蔽します。
📄 php-security.ini
expose_php = Off
server_tokens = Off
display_errors = Off
log_errors = On
【Prometheusの設定(prometheus.yml)】
データ収集エンジンに、「自分自身」「cAdvisor」「Node Exporter」の3箇所からデータを吸い上げるよう指示します。
📄 prometheus.yml
global:
scrape_interval: 15s
scrape_configs:
- job_name: 'prometheus'
static_configs:
- targets: ['localhost:9090']
- job_name: 'cadvisor'
static_configs:
- targets: ['cadvisor:8080']
- job_name: 'node-exporter'
static_configs:
- targets: ['node-exporter:9100']
💎 究極の docker-compose.yml
準備が整ったら、すべてを統合する設定ファイルを作成します。データベースやGrafanaの管理者には、指定したクレデンシャル(ユーザー名:admin_user / パスワード:YourStrongPass!23)を環境変数として直接流し込みます。
※ 本番環境ではより推測されにくいユーザー名と強力なパスワードに変更してください。
📄 docker-compose.yml
version: '3.8'
services:
# --- 1. データベース層 ---
db:
image: mariadb:10.11
container_name: mariadb
restart: always
environment:
MYSQL_ROOT_PASSWORD: 'YourStrongPass!23'
MYSQL_DATABASE: wordpress
MYSQL_USER: 'admin_user'
MYSQL_PASSWORD: 'YourStrongPass!23'
volumes:
- db_data:/var/lib/mysql
networks:
- wp-net
# --- 2. アプリケーション層 (WordPress) ---
wordpress:
image: wordpress:6-php8.3-apache
container_name: wordpress
restart: always
ports:
- "8080:80"
environment:
WORDPRESS_DB_HOST: db:3306
WORDPRESS_DB_USER: 'admin_user'
WORDPRESS_DB_PASSWORD: 'YourStrongPass!23'
WORDPRESS_DB_NAME: wordpress
volumes:
- wp_data:/var/www/html
# カスタムINIをマウントしてPHPバージョンを隠蔽
- ./php-security.ini:/usr/local/etc/php/conf.d/security.ini
networks:
- wp-net
depends_on:
- db
# --- 3. コンテナ管理 (Portainer) ---
portainer:
image: portainer/portainer-ce:latest
container_name: portainer
restart: always
ports:
- "9000:9000"
volumes:
- /var/run/docker.sock:/var/run/docker.sock
- portainer_data:/data
networks:
- wp-net
# --- 4. 監視エンジン (Prometheus) ---
prometheus:
image: prom/prometheus:latest
container_name: prometheus
restart: always
ports:
- "9090:9090"
volumes:
- ./prometheus.yml:/etc/prometheus/prometheus.yml
- prometheus_data:/prometheus
networks:
- wp-net
# --- 5. 監視ダッシュボード (Grafana) ---
grafana:
image: grafana/grafana-oss:latest
container_name: grafana
restart: always
ports:
- "3000:3000"
environment:
GF_SECURITY_ADMIN_USER: 'admin_user'
GF_SECURITY_ADMIN_PASSWORD: 'YourStrongPass!23'
volumes:
- grafana_data:/var/lib/grafana
networks:
- wp-net
depends_on:
- prometheus
# --- 6. コンテナリソース取得 (cAdvisor) ---
cadvisor:
image: gcr.io/cadvisor/cadvisor:v0.49.1
container_name: cadvisor
restart: always
privileged: true
ports:
- "8081:8080" # WPと被るため8081に変更
volumes:
- /:/rootfs:ro
- /var/run:/var/run:rw
- /sys:/sys:ro
- /var/lib/docker/:/var/lib/docker:ro
networks:
- wp-net
# --- 7. ホストリソース取得 (Node Exporter) ---
node-exporter:
image: prom/node-exporter:latest
container_name: node-exporter
restart: always
ports:
- "9100:9100"
networks:
- wp-net
networks:
wp-net:
driver: bridge
volumes:
db_data:
wp_data:
portainer_data:
prometheus_data:
grafana_data:
🚀 起動とアクセス
設定ファイルを保存したら、ついに魔法のコマンドを打ち込みます。
💻 Bash
docker compose up -d
このたった1行のコマンドで、7つのコンテナが協調し、仮想ネットワーク(wp-net)で繋がり、永続ボリュームが割り当てられます✨
🔗 各サービスのアクセス先:
- 🌐 http://サーバーIP:8080 $\rightarrow$ WordPressの初期設定画面。ここでサイトタイトルを入力し、DBと同じく
admin_userで管理者アカウントを作成すれば完了です。 - 📊 http://サーバーIP:3000 $\rightarrow$ Grafana。設定したID/PASSでログインし、Prometheusをデータソースに指定すれば、マシンの負荷状況やコンテナのメモリ使用率がサイバーでかっこいいグラフとして表示されます。
- 🚢 http://サーバーIP:9000 $\rightarrow$ Portainer。ターミナルを開かなくても、ブラウザ上からコンテナの再起動やログの確認が可能になります。
🎉 おわりに
ファイルシステムが Read-only になるという絶望的なエラーから始まり、原因の深堀り、k8s のパージ、そして Docker の再構築を経て、現代的でモダンな監視体制を備えた最高の WordPress 環境が完成しました🏆
インフラ運用の世界において、トラブルは最高の学習機会です。「なぜ動かないのか」をログ(dmesg, journalctl)から読み解き、カーネルの挙動や systemd の仕組みを理解して解決していくプロセスは、エンジニアリングの醍醐味そのものと言えるでしょう💡
この堅牢な環境の上で、ぜひ素晴らしいコンテンツを構築していってください!


