はじめに:なぜこの構成を選んだのか

現代の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

設定のポイント解説

  1. リソース制限: 各サービスにメモリ制限を設定することで、小規模なDropletでも安定動作を実現
  2. ネットワーク分離: ghost-networkにより、コンテナ間通信をセキュアに管理
  3. 依存関係管理: depends_onでサービス起動順序を制御し、データベース準備完了後にGhostを起動
  4. 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を使用する場合の設定手順:

  1. Cloudflareにドメインを追加
  2. Aレコードでサブドメインを設定:
    • blog1.example.com → DropletのIPアドレス
    • blog2.example.com → DropletのIPアドレス
  3. SSL/TLS設定を「Full」に変更
  4. 「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

Ubuntuサーバ(Droplets)×Docker Compose環境のGhostブログをCloudflare R2(rclone利用)へ自動バックアップする方法
はじめに DigitalOcean Droplets(Ubuntu)上で、Docker Composeを用いて複数のGhostブログおよびMySQLデータベースを運用している場合、障害やトラブルへの備えとして、定期的なバックアップおよびクラウドストレージへの保管は非常に重要です。 本記事では、Cloudflare R2をS3互換のバックアップ先として活用し、rcloneを利用して自動でバックアップ・アップロードを行う方法を解説します。 Digital Ocean DropletでDocker ComposeとGhostを使った複数ブログサイトの構築完全ガイドはじめに:なぜこの構成を選んだのか 現代のWebサイト運営において、複数のブログやサイトを効率的に管理することは多くの開発者やコンテンツクリエイターにとって重要な課題となっています。特に、個人ブログ、技術ブログ、企業サイトなど、異なる目的やターゲット読者を持つサイトを同時に運営する場合、それぞれを独立したサーバーで管理するのはコストと管理の観点から非効率的です。 本記事では、単一のDigital Ocean Droplet

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