普段の開発では Docker を使っていましたが、本番環境で運用したことがありま せんでした。せっかくなので自宅に余っていた小型 PC を使って、GitHub Actions による自動デプロイを構築してみました。
VPS ではなく自宅サーバを選んだ理由は以下の通りです。
- 小型 PC が何台か遊んでいた
- 同スペックの VPS と比較して月額料金がそこまで変わらない(消費電力を計測した結果)
- ルータレベルで VLAN 分離してセキュリティを確保できる
ネットワーク構成やセキュリティ設定については別記事で詳しく書いたので割愛します。
公開した Web アプリケーション
今回デプロイしたのは、GitHub API を使ってリポジトリのトラフィックやフォロワーの推移を記録するアプリケーションです。(※実際のアプリケーション:https://github-traffic.hn-tech-dev.com/)
主な機能は以下の通りです。
- リポジトリごとの訪問者数・ユニーク訪問者の日次記録
- フォロワー/フォローの推移グラフ
- マルチユーザー対応(各ユーザーが自分の GitHub トークンで管理)
GitHub API の制約上、トラフィックデータは 14 日間しか保持されないため、定期的に取得して DB に保存する必要があります。
環境構成
サーバ環境
- OS: Ubuntu 24.04.3 LTS
- Docker: 27.5.1
- Docker Compose: v2.32.4
開発環境と本番環境で同じ OS・Docker 環境を使用することで、環境差異によるトラブルを減らしました。
ネットワーク構成
外部からのアクセスはNginx Proxy Managerで SSL 終端とリバースプロキシを行い、各コンテナに振り分けています。
Nginx Proxy Managerを使用したネットワークの構築方法は以下の記事で紹介しています。
デプロイフローの全体像
- 開発者が
mainブランチに push - GitHub-hosted ランナーが Docker イメージをビルド → GHCR に push
- self-hosted ランナーが最新イメージを pull → 本番サーバで起動
- ヘルスチェック完了後、マイグレーションとキャッシュ最適化を実行
- デプロイ完了
全体で 3〜5 分程度で完了します(イメージサイズとネットワーク速度に依存)。
ダウンタイムについて
現在の実装ではdocker compose downから新コンテナのヘルスチェック完了まで数十秒のダウンタイムが発生します。
個人プロジェクトのため許容していますが、本格運用ではローリングア ップデートや Blue-Green デプロイを検討するのが良いと思われます。
ディレクトリ構成
開発環境用の.devcontainerは今回のスコープ外なので省略しています。
github-analytics-laravel/
├── .github/
│ └── workflows/
│ └── deploy-production.yml # CI/CDワークフロー
├── deploy/
│ ├── docker/
│ │ ├── mysql/
│ │ │ ├── Dockerfile
│ │ │ └── my.cnf
│ │ ├── nginx/
│ │ │ ├── Dockerfile
│ │ │ └── nginx.conf
│ │ └── php/
│ │ ├── Dockerfile # マルチステージビルド
│ │ ├── php-fpm.conf
│ │ └── docker-entrypoint.sh
│ ├── prod/
│ │ ├── docker-compose.yml
│ │ └── env.template
│ └── scripts/
│ └── deploy.sh # デプロイ自動化スクリプト
└── src/ # Laravelアプリケーション本体Docker イメージのビルド
本番用の PHP-FPM イメージはマルチステージビルドで最適化しました。
# ===========================================
# アセットビルド用ステージ
# ===========================================
FROM node:20-alpine AS assets
WORKDIR /app
COPY src/package*.json ./src/
RUN cd ./src && npm ci --ignore-scripts
COPY src ./src
RUN cd ./src && npm run build
# ===========================================
# PHP拡張ビルド用ステージ
# ===========================================
FROM php:8.2.29-fpm-alpine AS php-builder
# .build-depsという名前でパッケージ群をグループ化(後で一括削除するため)
# 参考:https://qiita.com/pottava/items/970d7b5cda565b995fe7
RUN apk add --no-cache --virtual .build-deps \
autoconf g++ make linux-headers \
# ↓「-dev」はヘッダファイル(コンパイル時のみ必要)
icu-dev oniguruma-dev libzip-dev \
&& docker-php-ext-install intl pdo_mysql mbstring zip bcmath \
&& pecl install redis \
&& docker-php-ext-enable redis \
# ↓ 同一RUN内で削除しないとレイヤーに残る(一括削除)
&& apk del .build-deps
# ===========================================
# Composer依存関係インストール用ステージ
# ===========================================
FROM composer:2 AS composer-deps
WORKDIR /app
COPY src/composer.json src/composer.lock ./
# --no-scriptsでpost-install-cmdをスキップ(.envが無い段階では実行できないため)
RUN composer install --no-dev --no-scripts --no-autoloader --prefer-dist --ignore-platform-reqs
COPY src .
# 本番環境用に最適化されたオートローダーを生成
RUN composer dump-autoload --optimize --classmap-authoritative --ignore-platform-reqs
# ===========================================
# 本番用ステージ
# ===========================================
FROM php:8.2.29-fpm-alpine AS prod
ENV TZ=Asia/Tokyo \
APP_ENV=production \
APP_DEBUG=0
# ランタイ ム依存のみインストール(ビルドツールは含まない)
RUN apk add --no-cache icu-libs libzip oniguruma tzdata
# ビルド済みPHP拡張をコピー
# extensions/: コンパイル済みの.soファイル(実体)
# conf.d/: extension=redis.soなどの設定ファイル(有効化)
COPY /usr/local/lib/php/extensions/ /usr/local/lib/php/extensions/
COPY /usr/local/etc/php/conf.d/ /usr/local/etc/php/conf.d/
WORKDIR /var/www/html
# ビルド済みアプリケーション・依存関係・アセットをコピー
COPY /app /var/www/html
COPY /app/src/public/build /var/www/html/public/build
# PHP-FPM設定(pm.max_childrenなどをチューニング済み)
COPY deploy/docker/php/php-fpm.conf /usr/local/etc/php-fpm.d/zzz-www.conf
# エントリーポイントスクリプト(storage権限設定やキャッシュクリアを実行)
COPY deploy/docker/php/docker-entrypoint.sh /usr/local/bin/docker-entrypoint.sh
RUN chmod +x /usr/local/bin/docker-entrypoint.sh
# Laravelが必要とするディレクトリを準備
RUN mkdir -p /var/run/php-fpm \
storage/logs storage/framework/cache \
storage/framework/sessions storage/framework/views \
bootstrap/cache \
&& rm -rf bootstrap/cache/*.php \
&& chown -R www-data:www-data /var/www/html /var/run/php-fpm \
&& chmod -R 775 storage bootstrap/cache
# コンテナヘルスチェック
HEALTHCHECK \
CMD pgrep php-fpm > /dev/null || exit 1
ENTRYPOINT ["/usr/local/bin/docker-entrypoint.sh"]
CMD []ポイント
マルチステージビルドを採用した理由
- 最終イメージにビルドツール(gcc、make など)を含めない → イメージサイズ削減
- Node.js、Composer、PHP 拡張を並列ビルド可能
- キャッシュレイヤーを最適化しやすい
ハマったポイント
composer install時に--no-scriptsを指定しないと、post-autoload-dumpイベントで
以下のartisanコマンドが実行されます。
"post-autoload-dump": [
"@php artisan config:clear",
"@php artisan clear-compiled",
"@php artisan package:discover --ansi"
]これらのコマンドは Laravel のブートストラップを必要としますが、このステージでは.envファイルや必要な PHP 拡張(pdo_mysqlなど)がまだ存在しないためエラーになります。--no-scriptsでスキップし、本番ステージで環境が整ってから実行します。
| オプション | 理由 |
|---|---|
--no-dev | 本番環境に phpunit 等の開発依存は不要 |
--no-scripts | .envがない段階でartisanコマンドを実行させない |
--no-autoloader | ソースコピー後に最適化版を生成するため一旦スキップ |
--classmap-authoritative | PSR-4 の動的解決を無効化し本番パフォーマンス向上 |
--ignore-platform-reqs | このステージには PHP 拡張がないためプラットフォーム要件を無視 |
--ignore-platform-reqsについて
composer-depsステージは composer 公式イメージを使用しており、pdo_mysqlやintlなどの PHP 拡張がインストールされていません。このオプションでプラットフォーム要件のチェックをスキップし、パッケージのダウンロードのみを行います。最終的な
prodステージにはphp-builderからコピーした拡張が揃っているため、実行時には問題なく動作します。
Nginx と MySQL の Dockerfile についてはリポジトリを参照してください。
GitHub Actions と self-hosted runner の設定
デプロイは self-hosted runner を使用して本番サーバ上で直接実行しています。
なぜ self-hosted runner を選んだか
通常のデプロイでは GitHub Actions のランナーから SSH 接続する必要があり、自宅サーバの SSH ポートを外部公開する必要があります。
self-hosted runner を使えば以下のメリットがあります。
- インバウンドのポート開放不要 - runner から GitHub へアウトバウンド接続するだけ
- SSH キー管理不要 - ランナーが本番サーバ上で直接実行
- ネットワーク構成がシンプル
セキュリティ的に有利なので採用しました。
self-hosted runner のセットアップ
GitHub リポジトリのSettings → Actions → Runners → New self-hosted runnerで表示されるコマンドを本番サーバで実行します。
このコマンドの中に、systemd サービスとして登録するためのスクリプトもダウンロードできます。
アーキテクチャは環境に合わせて選択します(今回は x64)。
初回はテストのため./run.shを手動実行し、問題なければ systemd サービスとして登録しました。
# systemdサービス化(例)
sudo ./svc.sh install
sudo ./svc.sh start
sudo ./svc.sh statusこれでシステム起動時に自動起動されます。
ワークフローファイルの構成
.github/workflows/deploy-production.ymlは 2 つのジョブで構成しています。
1. build-and-push ジョブ(GitHub-hosted ランナー)
Docker イメージをビルドして、GitHub Container Registry (GHCR) に push します。
env:
REGISTRY: ghcr.io
jobs:
build-and-push:
name: Build and Push Docker Images
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
image_name: ${{ steps.meta.outputs.image_name }}
timestamp: ${{ steps.meta.outputs.timestamp }}
sha_short: ${{ steps.meta.outputs.sha_short }}
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Generate metadata
id: meta
run: |
IMAGE_NAME=$(echo "${{ github.repository }}" | tr '[:upper:]' '[:lower:]')
TIMESTAMP=$(date +%Y%m%d-%H%M%S)
SHA_SHORT=$(echo "${{ github.sha }}" | cut -c1-7)
echo "image_name=${IMAGE_NAME}" >> $GITHUB_OUTPUT
echo "timestamp=${TIMESTAMP}" >> $GITHUB_OUTPUT
echo "sha_short=${SHA_SHORT}" >> $GITHUB_OUTPUT
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to GitHub Container Registry
uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push App image
uses: docker/build-push-action@v5
with:
context: .
file: ./deploy/docker/php/Dockerfile
push: true
tags: |
${{ env.REGISTRY }}/${{ steps.meta.outputs.image_name }}/app:latest
${{ env.REGISTRY }}/${{ steps.meta.outputs.image_name }}/app:${{ steps.meta.outputs.timestamp }}
${{ env.REGISTRY }}/${{ steps.meta.outputs.image_name }}/app:${{ steps.meta.outputs.sha_short }}
cache-from: type=gha
cache-to: type=gha,mode=max
target: prod
- name: Build and push Web image
uses: docker/build-push-action@v5
with:
context: .
file: ./deploy/docker/nginx/Dockerfile
push: true
tags: |
${{ env.REGISTRY }}/${{ steps.meta.outputs.image_name }}/web:latest
${{ env.REGISTRY }}/${{ steps.meta.outputs.image_name }}/web:${{ steps.meta.outputs.timestamp }}
${{ env.REGISTRY }}/${{ steps.meta.outputs.image_name }}/web:${{ steps.meta.outputs.sha_short }}
cache-from: type=gha
cache-to: type=gha,mode=max
- name: Build and push DB image
uses: docker/build-push-action@v5
with:
context: .
file: ./deploy/docker/mysql/Dockerfile
push: true
tags: |
${{ env.REGISTRY }}/${{ steps.meta.outputs.image_name }}/db:latest
${{ env.REGISTRY }}/${{ steps.meta.outputs.image_name }}/db:${{ steps.meta.outputs.timestamp }}
${{ env.REGISTRY }}/${{ steps.meta.outputs.image_name }}/db:${{ steps.meta.outputs.sha_short }}
cache-from: type=gha
cache-to: type=gha,mode=maxイメージタグ戦略
イメージは 3 種類のタグで push しています。
latest: 常に最新版を指す(通常はこれを使用)20251230-143022: デプロイ時刻を 記録(タイムスタンプ)a1b2c3d: コミット SHA の短縮版
これにより、問題が発生した際に過去の特定バージョンへ簡単にロールバックできます。
キャッシュ戦略
cache-from: type=ghaとcache-to: type=gha,mode=maxで GitHub Actions のキャッシュを使用しています。2 回目以降のビルドが大幅に高速化されます(初回 15 分 → 2 回目以降 3 分程度)。
2. deploy ジョブ(self-hosted ランナー)
本番サーバで実行され、実際のデプロイを行います。
deploy:
name: Deploy to Production Server
needs: build-and-push
runs-on: self-hosted
# mainブランチのみデプロイを実行
if: github.ref == 'refs/heads/main'
env:
GHCR_TOKEN: ${{ secrets.GHCR_TOKEN }}
GHCR_USERNAME: ${{ secrets.GHCR_USERNAME }}
steps:
- name: Checkout deployment scripts
uses: actions/checkout@v4
with:
sparse-checkout: |
deploy/scripts
deploy/prod
- name: Setup production .env file
env:
PRODUCTION_ENV: ${{ secrets.PRODUCTION_ENV }}
run: |
DEPLOY_DIR="/home/$(whoami)/deploy/github-analytics"
mkdir -p "$DEPLOY_DIR"
# .envファイルをbase64デコードして配置
if [ -n "$PRODUCTION_ENV" ]; then
echo "$PRODUCTION_ENV" | base64 --decode > "$DEPLOY_DIR/.env"
echo "✅ .env file created from secrets"
else
echo "⚠️ PRODUCTION_ENV secret not found"
if [ ! -f "$DEPLOY_DIR/.env" ]; then
echo "❌ .env file does not exist"
exit 1
fi
echo "ℹ️ Using existing .env file"
fi
- name: Deploy application
run: |
cd deploy/scripts
chmod +x deploy.sh
./deploy.sh
- name: Deployment summary
run: |
echo "## ✅ Deployment Completed" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Timestamp:** $(date '+%Y-%m-%d %H:%M:%S')" >> $GITHUB_STEP_SUMMARY
echo "**Commit:** \`${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARYsparse-checkout を使う理由
全リポジトリを clone すると時間がかかるため、デプロイに必要なdeploy/scriptsとdeploy/prodのみチェックアウトしています。デプロイ時間を短縮できます。
環境変数ファイルの扱い
.envは Git に含めないため、GitHub Secrets に保存した内容を base64 デコードして配置します。
また、GitHub Secrets は改行を含む値を正しく扱えない場合があるため、base64 エンコードで 1 行の文字列として保存しています。
# Secretsに登録する際
cat .env | base64 -w 0登録方法などは以下の記事で解説していますので参考にしてください。
必要な GitHub Secrets
以下をリポジトリの Secrets に設定します。
| Secret 名 | 説明 |
|---|---|
GHCR_TOKEN | Personal Access Token(read:packages権限) |
GHCR_USERNAME | GitHub ユーザー名 |
PRODUCTION_ENV | .envファイルの内容(base64 エンコード済み) |
GHCR_TOKENは、GitHub Container Registry から非公開イメージを pull するために必要です。
また、プロフィールのSettings → Developer Settings → Personal access tokens (classic) で以下の項目に対する許可を追加しトークンを取得します。
取得したトークンはGHCR_TOKENの Secrets に設定してください。
ワークフロートリガー
mainブランチへの push: ビルド + デプロイ実行developブランチへの push: ビルドのみ実行- 手動実行: GitHub Actions UI から
workflow_dispatchで実行可能
これにより、開発ブランチでイメージビルドをテストし main マージ時に自動デプロイできます。
docker-compose.yml の構成
本番環境用のdocker-compose.ymlは 6 つのサービスで構成しています。
volumes:
db-store:
name: github-analytics_db
php-fpm-socket:
name: github-analytics_php-fpm-socket
app-storage:
name: github-analytics_app-storage
app-public:
name: github-analytics_app-public
redis-data:
name: github-analytics_redis
networks:
nginx-proxy-manager-network:
external: true
services:
app:
container_name: github-analytics-backend
image: ${REGISTRY_URL}/app:${IMAGE_TAG:-latest}
environment:
APP_ENV: production
APP_DEBUG: "false"
TZ: Asia/Tokyo
env_file:
- .env
volumes:
- php-fpm-socket:/var/run/php-fpm
- app-storage:/var/www/html/storage
- app-public:/var/www/html/public
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "php -v >/dev/null 2>&1 || exit 1"]
interval: 30s
timeout: 5s
retries: 3
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
networks:
- nginx-proxy-manager-network
web:
container_name: github-analytics-web
image: ${REGISTRY_URL}/web:${IMAGE_TAG:-latest}
depends_on:
- app
expose:
- "80"
volumes:
- php-fpm-socket:/var/run/php-fpm
- app-public:/var/www/html/public:ro
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost/ >/dev/null 2>&1 || exit 1"]
interval: 30s
timeout: 5s
retries: 3
networks:
- nginx-proxy-manager-network
db:
container_name: github-analytics-db
image: ${REGISTRY_URL}/db:${IMAGE_TAG:-latest}
volumes:
- db-store:/var/lib/mysql
env_file:
- .env
environment:
TZ: ${TIME_ZONE}
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
restart: unless-stopped
healthcheck:
test: ["CMD-SHELL", "mysqladmin ping -h localhost -u$${MYSQL_USER} -p$${MYSQL_PASSWORD} --silent"]
interval: 30s
timeout: 5s
retries: 5
networks:
- nginx-proxy-manager-network
redis:
container_name: github-analytics-redis
image: redis:7-alpine
volumes:
- redis-data:/data
command: redis-server --appendonly yes --requirepass "${REDIS_PASSWORD:-}"
restart: unless-stopped
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 30s
timeout: 3s
retries: 3
networks:
- nginx-proxy-manager-network
# Laravel Scheduler
scheduler:
container_name: github-analytics-scheduler
image: ${REGISTRY_URL}/app:${IMAGE_TAG:-latest}
user: "www-data:www-data"
entrypoint: []
command: php artisan schedule:work --verbose
environment:
APP_ENV: production
APP_DEBUG: "false"
TZ: Asia/Tokyo
env_file:
- .env
volumes:
- app-storage:/var/www/html/storage
restart: unless-stopped
depends_on:
db:
condition: service_healthy
redis:
condition: service_healthy
networks:
- nginx-proxy-manager-network
# phpMyAdmin(データベース管理用)
# ローカルホストからのみアクセス可能
phpmyadmin:
image: phpmyadmin:latest
container_name: github-analytics-phpmyadmin
ports:
- "8091:80"
environment:
PMA_ARBITRARY: 1
PMA_HOST: db
PMA_PORT: 3306
PMA_USER: ${MYSQL_USER}
PMA_PASSWORD: ${MYSQL_PASSWORD}
MYSQL_ROOT_PASSWORD: ${MYSQL_ROOT_PASSWORD}
depends_on:
db:
condition: service_healthy
restart: unless-stopped
networks:
- nginx-proxy-manager-networkscheduler コンテナの起動方法
scheduler コンテナはappと同じイメージを使用しますが、起動方法が異なります。
scheduler:
image: ${REGISTRY_URL}/app:${IMAGE_TAG:-latest}
entrypoint: [] # Dockerfileで定義したENTRYPOINTを無効化
command: php artisan schedule:work --verbose # 直接コマンドを実行entrypoint: []で Dockerfile のENTRYPOINT(docker-entrypoint.sh)を無効化し、commandで直接php artisan schedule:workを実行しています。
これにより、同じイメージから異なるプロセスを起動できます。
アーキテクチャの特徴
1 コンテナ 1 プロセス原則
app: PHP-FPM(Web アプリケーション本体)scheduler: Laravel Scheduler(schedule:work)web: Nginx(リバースプロキシ)db: MySQLredis: Redis(キャッシュ・セッション・キュー)phpmyadmin: phpMyAdmin(データベース管理用、ローカルアクセスのみ)
scheduler を別コンテナに分離することで、アプリケーションコンテナの再起動時にスケジューラーが停止しません。
ヘルスチェック依存関係
depends_onにcondition: service_healthyを指定することで、DB が完全に起動してからアプリケーションを起動できます。
ネットワーク分離
すべてのコンテナはnginx-proxy-manager-networkという外部ネットワークに接続しています。このネットワークは Nginx Proxy Manager と共有しており、リバースプロキシ経由で外部からのアクセスを受け付けます。
デプロイスクリプトの詳細
deploy/scripts/deploy.shがデプロイの実処理を行います。
#!/bin/bash
###############################################################################
# GitHub Analytics - Production Deployment Script
###############################################################################
# このスクリプトは本番環境へのデプロイを自動化します。
# 主な処理内容:
# 1. デプロイディレクトリの準備とdocker-compose.ymlのコピー
# 2. GitHub Container Registryへのログイン
# 3. 最新のDockerイメージの取得
# 4. 既存コンテナの停止と新規コンテナの起動
# 5. サービスヘルスチェック待機
# 6. データベース接続確認とマイグレーション実行
# 7. 管理者ユーザーの作成(シーダー実行)
# 8. Livewireアセットの公開
# 9. Laravelキャッシュの最適化
# 10. 古いDockerイメージのクリーンアップ
set -e # エラーで停止
# カラー出力用の定義
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# ログ出力関数
log_info() {
echo -e "${BLUE}[INFO]${NC} $1"
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# ============================================================================
# 1. デプロイディレクトリの準備
# ============================================================================
# デプロイディレクトリへ移動
DEPLOY_DIR="/home/$(whoami)/deploy/github-analytics"
COMPOSE_FILE="$DEPLOY_DIR/docker-compose.yml"
log_info "Deployment started at $(date '+%Y-%m-%d %H:%M:%S')"
# デプロイディレクトリが存在しない場合は作成
if [ ! -d "$DEPLOY_DIR" ]; then
log_warning "Deploy directory does not exist. Creating: $DEPLOY_DIR"
mkdir -p "$DEPLOY_DIR"
fi
# docker-compose.ymlをコピー
log_info "Copying docker-compose.yml to $DEPLOY_DIR"
cp -f ../prod/docker-compose.yml "$DEPLOY_DIR/"
# .envファイルの存在確認(初回デプロイ時の警告)
if [ ! -f "$DEPLOY_DIR/.env" ]; then
log_error ".env file not found in $DEPLOY_DIR"
log_error "Please create .env file based on env.template before deployment"
log_error "Run: cp ../prod/env.template $DEPLOY_DIR/.env"
exit 1
fi
# 現在のディレクトリを変更
cd "$DEPLOY_DIR"
# ============================================================================
# 2. GitHub Container Registryへのログイン
# ============================================================================
# GitHub Container Registryにログイン
log_info "Logging in to GitHub Container Registry..."
if [ -n "$GHCR_TOKEN" ]; then
echo "$GHCR_TOKEN" | docker login ghcr.io -u "$GHCR_USERNAME" --password-stdin
log_success "Logged in to ghcr.io"
else
log_warning "GHCR_TOKEN not set. Attempting to use cached credentials..."
fi
# ============================================================================
# 3. 最新のDockerイメージの取得
# ============================================================================
# 最新のイメージをPull
log_info "Pulling latest Docker images from registry..."
docker compose pull
# ============================================================================
# 4. 既存コンテナの停止と新規コンテナの起動
# ============================================================================
# コンテナの停止と削除(データベースボリュームは保持)
log_info "Stopping existing containers..."
docker compose down --remove-orphans
# コンテナの起動
log_info "Starting containers..."
docker compose up -d
# ============================================================================
# 5. サービスヘルスチェック待機
# ============================================================================
# ヘルスチェック待機
log_info "Waiting for services to be healthy..."
MAX_WAIT=60
WAIT_COUNT=0
while [ $WAIT_COUNT -lt $MAX_WAIT ]; do
DB_HEALTHY=$(docker compose ps db --format json | grep -o '"Health":"[^"]*"' | cut -d'"' -f4)
APP_HEALTHY=$(docker compose ps app --format json | grep -o '"Health":"[^"]*"' | cut -d'"' -f4)
if [ "$DB_HEALTHY" = "healthy" ] && [ "$APP_HEALTHY" = "healthy" ]; then
log_success "All services are healthy!"
break
fi
WAIT_COUNT=$((WAIT_COUNT + 1))
log_info "Waiting for services... ($WAIT_COUNT/$MAX_WAIT) - DB: $DB_HEALTHY, App: $APP_HEALTHY"
sleep 2
done
if [ $WAIT_COUNT -eq $MAX_WAIT ]; then
log_error "Services did not become healthy in time!"
docker compose ps
docker compose logs --tail=50 app
docker compose logs --tail=50 db
exit 1
fi
# 設定キャッシュをクリア(実行時の環境変数を使用するため)
log_info "Clearing configuration cache..."
docker compose exec -T app php artisan config:clear || true
# ============================================================================
# 6. データベース接続確認
# ============================================================================
# データベース接続確認(追加の安全チェック)
# 注意: 接続確認が失敗しても、マイグレーション処理は実行されます
log_info "Verifying database connection..."
MAX_DB_RETRIES=15
DB_RETRY_COUNT=0
DB_CONNECTION_OK=false
while [ $DB_RETRY_COUNT -lt $MAX_DB_RETRIES ]; do
DB_RETRY_COUNT=$((DB_RETRY_COUNT + 1))
# 最後のリトライではエラー内容を表示
if [ $DB_RETRY_COUNT -eq $MAX_DB_RETRIES ]; then
log_info "Final connection attempt with detailed output..."
if docker compose exec -T app php artisan db:show; then
log_success "Database connection verified!"
DB_CONNECTION_OK=true
break
else
log_warning "Could not establish database connection at this stage!"
log_info "This may be normal if MySQL is still initializing."
log_info "Will attempt to run migrations anyway..."
log_info "Checking environment variables..."
docker compose exec -T app env | grep -E "(DB_|MYSQL_)" | grep -v PASSWORD
# 接続確認が失敗しても続行(exit 1を削除)
break
fi
else
if docker compose exec -T app php artisan db:show > /dev/null 2>&1; then
log_success "Database connection verified!"
DB_CONNECTION_OK=true
break
fi
log_info "Database connection check... retry $DB_RETRY_COUNT/$MAX_DB_RETRIES"
sleep 3
fi
done
if [ "$DB_CONNECTION_OK" = false ]; then
log_warning "Database connection verification failed, but continuing with migration attempt..."
fi
# ============================================================================
# 7. データベースマイグレーション実行
# ============================================================================
# データベースマイグレーション実行(接続確認が失敗した場合でも試行)
log_info "Running database migrations..."
MAX_MIGRATION_RETRIES=10
MIGRATION_RETRY_COUNT=0
MIGRATION_SUCCESS=false
while [ $MIGRATION_RETRY_COUNT -lt $MAX_MIGRATION_RETRIES ]; do
MIGRATION_RETRY_COUNT=$((MIGRATION_RETRY_COUNT + 1))
if docker compose exec -T app php artisan migrate --force; then
log_success "Database migrations completed successfully"
MIGRATION_SUCCESS=true
break
else
if [ $MIGRATION_RETRY_COUNT -lt $MAX_MIGRATION_RETRIES ]; then
log_warning "Migration attempt $MIGRATION_RETRY_COUNT/$MAX_MIGRATION_RETRIES failed. Retrying..."
sleep 5
else
log_error "Database migrations failed after $MAX_MIGRATION_RETRIES attempts!"
log_info "Checking app container logs..."
docker compose logs --tail=50 app
log_info "Checking db container logs..."
docker compose logs --tail=30 db
exit 1
fi
fi
done
# ============================================================================
# 8. 管理者ユーザーの作成(シーダー実行)
# ============================================================================
# 管理者ユーザーの作成(初回デプロイ時 or 未作成時)
log_info "Running AdminUserSeeder..."
if docker compose exec -T app php artisan db:seed --class=AdminUserSeeder --force; then
log_success "AdminUserSeeder completed successfully"
else
log_error "AdminUserSeeder failed!"
log_info "Checking app container logs..."
docker compose logs --tail=50 app
exit 1
fi
# ============================================================================
# 9. Livewireアセットの公開
# ============================================================================
# Livewireの静的アセットを公開(JavaScriptファイルなど)
log_info "Publishing Livewire assets..."
if docker compose exec -T app php artisan livewire:publish --force; then
log_success "Livewire assets published successfully"
else
log_warning "Failed to publish Livewire assets, but continuing..."
fi
# ============================================================================
# 10. Laravelキャッシュの最適化
# ============================================================================
# キャッシュクリア&最適化
log_info "Clearing and optimizing caches..."
docker compose exec -T app php artisan config:cache
docker compose exec -T app php artisan route:cache
docker compose exec -T app php artisan view:cache
docker compose exec -T app php artisan optimize
# コンテナステータス確認
log_info "Checking container status..."
docker compose ps
# ログ確認(最後の20行)
log_info "Recent logs:"
docker compose logs --tail=20
log_success "Deployment completed successfully at $(date '+%Y-%m-%d %H:%M:%S')"
log_info "Application is running at the configured domain"
# ============================================================================
# 11. 古いDockerイメージのクリーンアップ
# ============================================================================
# クリーンアップ(古いイメージの削除)
log_info "Cleaning up old Docker images..."
docker image prune -f
log_success "All done! 🚀"docker compose exec -Tのコマンドですが、GitHub Actions のような non-interactive 環境では TTY が割り当てられないため、-T で擬似 TTY を無効化する必要があります。これを省略するとthe input device is not a TTYエラーになります。



