はじめに:なぜこの構成を選んだのか
現代のWebサイト運営において、複数のブログやサイトを効率的に管理することは多くの開発者やコンテンツクリエイターにとって重要な課題となっています。特に、個人ブログ、技術ブログ、企業サイトなど、異なる目的やターゲット読者を持つサイトを同時に運営する場合、それぞれを独立したサーバーで管理するのはコストと管理の観点から非効率的です。
本記事では、単一のDigital Ocean Droplet上でDocker Composeを活用し、Ghostプラットフォームによる複数ブログサイトを構築する方法を詳しく解説します。この構成により、月額わずか数ドルの小規模サーバーでプロフェッショナルなブログサイトを複数運営できるようになります。また、CaddyをリバースプロキシとしてHTTPS自動化やキャッシュ最適化を実現し、MySQLデータベースで各ブログのデータを分離管理する包括的なソリューションを提供します。
システム概要と技術選択の背景
利用サービスとその選択理由
今回の構成では、以下のサービスとツールを組み合わせて使用します:
インフラストラクチャ層
- Digital Ocean Droplets: コストパフォーマンスに優れたVPSサービス
- お名前.com: 日本国内での信頼性が高いドメイン登録サービス
- Cloudflare: CDNとDNS管理による高速化とセキュリティ強化
- Mailgun: 高い到達率を誇るトランザクションメール配信サービス
- GitHub: ソースコード管理とバージョン管理
アプリケーション層
- Ghost: モダンで高速なオープンソースCMS
- MySQL 8.0: 信頼性の高いリレーショナルデータベース
- Caddy: 自動HTTPS機能を備えたWebサーバー
- Docker Compose: コンテナオーケストレーション
この技術スタックの選択理由として、Ghostは従来のWordPressと比較してパフォーマンスが優秀で、現代的なマークダウンベースの執筆体験を提供します。Caddyは設定が簡潔でLet's Encrypt証明書の自動更新機能があり、運用負荷を大幅に削減できます。Docker Composeにより、開発環境と本番環境の一貫性を保ちながら、スケーラブルなアーキテクチャを実現しています。
完全なファイル構成と詳細解説
プロジェクト構造の全体像
/var/www/ghost-blogs/
├── docker-compose.yml
├── Caddyfile
├── .env
├── .gitignore
├── README.md
├── mysql-init/
│ └── init.sql
└── volumes/
├── caddy_data/
├── caddy_config/
├── database/
├── content-blog1/
└── content-blog2/
この構造により、各コンポーネントが明確に分離され、保守性と拡張性を確保しています。
Docker Compose設定の完全版(docker-compose.yml)
version: '3.8'
services:
caddy:
image: caddy:2-alpine
restart: unless-stopped
ports:
- "80:80"
- "443:443"
volumes:
- ./Caddyfile:/etc/caddy/Caddyfile
- ./volumes/caddy_data:/data
- ./volumes/caddy_config:/config
networks:
- ghost-network
depends_on:
- ghost-blog1
- ghost-blog2
deploy:
resources:
limits:
memory: 100M
ghost-blog1:
image: ghost:5-alpine
restart: unless-stopped
volumes:
- ./volumes/content-blog1:/var/lib/ghost/content
environment:
NODE_ENV: production
database__client: mysql
database__connection__host: database
database__connection__user: ghost_user
database__connection__password: ${DB_PASSWORD}
database__connection__database: ghost_blog1
url: https://blog1.example.com
caching__frontend__maxAge: 60
mail__transport: "SMTP"
mail__options__service: "Mailgun"
mail__options__host: "smtp.mailgun.org"
mail__options__port: 2525
mail__options__secure: "false"
mail__options__auth__user: "${MAILGUN_USER}"
mail__options__auth__pass: "${MAILGUN_PASSWORD}"
mail__from: "[email protected]"
networks:
- ghost-network
depends_on:
- database
deploy:
resources:
limits:
memory: 128M
ghost-blog2:
image: ghost:5-alpine
restart: unless-stopped
volumes:
- ./volumes/content-blog2:/var/lib/ghost/content
environment:
NODE_ENV: production
database__client: mysql
database__connection__host: database
database__connection__user: ghost_user
database__connection__password: ${DB_PASSWORD}
database__connection__database: ghost_blog2
url: https://blog2.example.com
caching__frontend__maxAge: 60
mail__transport: "SMTP"
mail__options__service: "Mailgun"
mail__options__host: "smtp.mailgun.org"
mail__options__port: 2525
mail__options__secure: "false"
mail__options__auth__user: "${MAILGUN_USER}"
mail__options__auth__pass: "${MAILGUN_PASSWORD}"
mail__from: "[email protected]"
networks:
- ghost-network
depends_on:
- database
deploy:
resources:
limits:
memory: 128M
database:
image: mysql:8.0
restart: unless-stopped
environment:
MYSQL_ROOT_PASSWORD: ${ROOT_PASSWORD}
MYSQL_USER: ghost_user
MYSQL_PASSWORD: ${DB_PASSWORD}
MYSQL_DATABASE: ghost_common
volumes:
- ./volumes/database:/var/lib/mysql
- ./mysql-init:/docker-entrypoint-initdb.d
networks:
- ghost-network
command:
- --default-authentication-plugin=mysql_native_password
- --character-set-server=utf8mb4
- --collation-server=utf8mb4_unicode_ci
deploy:
resources:
limits:
memory: 256M
volumes:
ghost-database:
ghost-blog1-data:
ghost-blog2-data:
networks:
ghost-network:
driver: bridge
設定のポイント解説
- リソース制限: 各サービスにメモリ制限を設定することで、小規模なDropletでも安定動作を実現
- ネットワーク分離:
ghost-network
により、コンテナ間通信をセキュアに管理 - 依存関係管理:
depends_on
でサービス起動順序を制御し、データベース準備完了後にGhostを起動 - Alpine Linux採用: 軽量なAlpineベースイメージでリソース使用量を最小化
Caddy設定ファイル(Caddyfile)
{
email [email protected]
}
blog1.example.com {
@cacheable_content_api {
path /ghost/api/content/*
}
header @cacheable_content_api Cache-Control "public, max-age=3, s-maxage=3"
reverse_proxy ghost-blog1:2368
}
blog2.example.com {
@cacheable_content_api {
path /ghost/api/content/*
}
header @cacheable_content_api Cache-Control "public, max-age=3, s-maxage=3"
reverse_proxy ghost-blog2:2368
}
Caddy設定の技術的解説
- 自動HTTPS: Let's Encryptによる証明書の自動取得・更新
- マッチャー機能:
@cacheable_content_api
でAPIエンドポイントを識別 - キャッシュ戦略: コンテンツAPIに短時間キャッシュを適用してパフォーマンス向上
- リバースプロキシ: ドメインベースでのルーティング実現
データベース初期化スクリプト(mysql-init/init.sql)
CREATE DATABASE IF NOT EXISTS ghost_blog1 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
GRANT ALL PRIVILEGES ON ghost_blog1.* TO 'ghost_user'@'%';
CREATE DATABASE IF NOT EXISTS ghost_blog2 CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;
GRANT ALL PRIVILEGES ON ghost_blog2.* TO 'ghost_user'@'%';
FLUSH PRIVILEGES;
データベース設計のポイント
- UTF-8MB4文字セット: 絵文字を含む多言語文字の完全サポート
- データベース分離: 各ブログが独立したデータベースを使用
- 権限最小化: 各データベースに対する必要最小限の権限のみ付与
環境変数ファイル(.env)
# MySQL設定
ROOT_PASSWORD=your_secure_mysql_root_password_here
DB_PASSWORD=your_secure_ghost_db_password_here
# Mailgun設定(メール機能用)
[email protected]
MAILGUN_PASSWORD=your_mailgun_password_here
Gitignore設定(.gitignore)
# 環境変数ファイル
.env
# データボリューム
volumes/
# ログファイル
*.log
# OS固有ファイル
.DS_Store
Thumbs.db
# エディタ設定
.vscode/
.idea/
# 一時ファイル
*.tmp
*.temp
詳細な構築手順
1. Digital Ocean Droplet初期設定
最小構成によるメモリ不足対策
Digital Ocean Dropletの最小構成(1GB RAM)では、GhostとMySQLを同時実行するとメモリ不足が発生する可能性があります。この問題を解決するため、swapファイルを作成します:
# 2GBのswapファイル作成
sudo fallocate -l 2G /swapfile
# セキュリティのためファイル権限設定
sudo chmod 600 /swapfile
# swapファイルとして初期化
sudo mkswap /swapfile
# swapを有効化
sudo swapon /swapfile
# 永続化設定(再起動後も有効)
echo '/swapfile none swap sw 0 0' | sudo tee -a /etc/fstab
# 設定確認
sudo swapon --show
free -h
この設定により、物理メモリが不足した際にディスク領域を仮想メモリとして使用でき、システムの安定性が向上します。
2. 必要なパッケージのインストール
# システム更新
sudo apt update && sudo apt upgrade -y
# Docker公式リポジトリ追加
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg
echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null
# Docker & Docker Composeインストール
sudo apt update
sudo apt install docker-ce docker-ce-cli containerd.io docker-compose-plugin -y
# Dockerサービス開始・自動起動設定
sudo systemctl start docker
sudo systemctl enable docker
# 現在のユーザーをdockerグループに追加
sudo usermod -aG docker $USER
3. プロジェクトセットアップ
# プロジェクトディレクトリ作成
mkdir -p /var/www/ghost-blogs/{mysql-init,volumes}
cd /var/www/ghost-blogs
# 必要なディレクトリ作成
mkdir -p volumes/{caddy_data,caddy_config,database,content-blog1,content-blog2}
# 設定ファイル作成
touch docker-compose.yml Caddyfile .env .gitignore README.md
touch mysql-init/init.sql
4. ファイアウォール設定
# UFW(Uncomplicated Firewall)インストール・設定
sudo ufw --force reset
sudo ufw default deny incoming
sudo ufw default allow outgoing
# 必要なポートのみ開放
sudo ufw allow 22 # SSH
sudo ufw allow 80 # HTTP
sudo ufw allow 443 # HTTPS
# ファイアウォール有効化
sudo ufw --force enable
sudo ufw status verbose
5. DNS設定(Cloudflare使用例)
Cloudflareを使用する場合の設定手順:
- Cloudflareにドメインを追加
- Aレコードでサブドメインを設定:
blog1.example.com
→ DropletのIPアドレスblog2.example.com
→ DropletのIPアドレス
- SSL/TLS設定を「Full」に変更
- 「Always Use HTTPS」を有効化
6. サービス起動と初期設定
# バックグラウンドでサービス起動
docker compose up -d
# 起動状況確認
docker compose ps
docker compose logs -f
### 7. Ghost初期設定
各ブログサイトの管理画面にアクセスして設定を行います:
**Blog1の設定**
1. `https://blog1.example.com/ghost`にアクセス
2. 管理者アカウント作成(メールアドレス、パスワード設定)
3. サイト設定(タイトル、説明、言語設定)
4. テーマ選択とカスタマイズ
**Blog2の設定**
1. `https://blog2.example.com/ghost`にアクセス
2. 同様に管理者アカウントとサイト設定を実施
## 結論とベストプラクティス
本記事で紹介した構成により、月額$6程度のDigital Ocean Dropletで複数のプロフェッショナルなGhostブログサイトを運営できます。重要なポイントとして、適切なリソース管理、セキュリティ設定、バックアップ戦略が安定運用の鍵となります。
**運用における推奨事項**
1. 定期的なセキュリティアップデート実施
2. 自動バックアップスケジュール設定
3. ログローテーション設定
4. パフォーマンス監視の導入
5. 災害復旧計画の策定
この構成をベースとして、トラフィック増加に応じてDropletのスペックアップやロードバランサーの追加など、段階的なスケーリングが可能です。また、コンテナベースのアーキテクチャにより、開発環境との一貫性を保ちながら、継続的な改善とアップデートを実現できます。
Citations:
[1] https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/14147688/44817932-1928-4769-a1ff-af226b481beb/Caddyfile-3.txt
[2] https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/14147688/87da025e-894b-40cf-b805-349be7916fba/docker-compose-10.yml
[3] https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/14147688/a612fb01-38fc-43c6-a309-97566cadddd4/README-1.md
[4] https://ppl-ai-file-upload.s3.amazonaws.com/web/direct-files/attachments/14147688/92091220-26fd-41ac-bdca-3289f56952e1/init-1.sql
[5] https://pplx-res.cloudinary.com/image/private/user_uploads/14147688/9dfa17a1-fdcb-4caf-bdde-d6567cec71a2/image.jpg

バックアップの設定記事はこちら