2026-02-19
この記事をシェア
はじめに — なぜDifyをAWSに載せるのか
Difyは、LLMアプリケーションを素早く構築できるオープンソースプラットフォームです。RAGパイプライン、ワークフロー、エージェント機能を備え、docker compose upで簡単に起動できるのが魅力ですが、本番環境で運用するとなると話は別です。
公式のdocker-compose.yamlは、PostgreSQL・Redis・Weaviate・RabbitMQ・nginxなど10個以上のコンテナを1台のサーバーで動かす前提です。これでは:
- DBが壊れたらすべてのデータが消える
- サーバー1台に障害が起きるとサービス全停止
- トラフィック増加時にスケールできない
- バックアップ・パッチ適用を手動で管理する必要がある
本記事では、Dify 1.13.0をAWS App Runner上にデプロイし、ステートフルな部分をすべてAWSマネージドサービスに外出しすることで、スケーラブルで運用コストの低い構成を実現した方法を、Terraformコード付きで徹底解説します。
💡 この記事で得られること
- App Runner / ECS / EKS の使い分け判断基準
- 「ステートレス vs ステートフル」で考えるクラウド設計の鉄則
- Dify 1.13.0のAll-in-Oneコンテナ構築の全ソースコード
- 実際にハマった10個のポイントと解決策
- Terraform モジュール構成の全体像
なぜApp Runnerを選んだのか
AWSでコンテナを動かす3つの選択肢
| 項目 | App Runner | ECS Fargate | EKS |
|---|---|---|---|
| 設定の複雑さ | ⭐ 最も簡単 | ⭐⭐ やや複雑 | ⭐⭐⭐ 最も複雑 |
| ALB(ロードバランサ) | 不要(組み込み) | 別途必要 | 別途必要 |
| TLS/HTTPS | 自動 | ACM + ALBで設定 | Ingress設定 |
| オートスケール | 組み込み | 設定が必要 | HPA設定が必要 |
| VPC統合 | VPC Connector | ネイティブ | ネイティブ |
| 最小コスト(開発環境) | ~$30/月 | ~$50/月(ALB込み) | ~$150/月(クラスタ+ALB) |
| Terraformリソース数 | 5-8 | 15-20 | 30+ |
App Runnerを選んだ3つの理由
1. ALBが不要 — コストと複雑さを削減
ECS Fargateを使う場合、HTTPSでのアクセスにはALB(Application Load Balancer)が必須で、それだけで月額約$20。App RunnerはHTTPSとロードバランシングが組み込みなので丸ごと不要です。
2. デプロイが圧倒的にシンプル
App Runnerは「ECRにイメージをpush → デプロイ開始」するだけ。ECSだとタスク定義の更新 → サービスの更新 → デプロイ待ちと手順が多い。Terraformリソース数もECSの半分以下です。
3. 開発環境には十分すぎる
今回の用途は開発・検証環境です。本番で大量のリクエストを捌く必要が出てきたらECSへの移行を検討しますが、開発段階では「動くまでの最短距離」を優先。App Runnerからの卒業は、Terraformでインフラコード管理しているので容易です。
⚠️ App Runnerの制約
- コンテナは1つだけ — サイドカーパターンは使えない
- WebSocketは非対応 — リアルタイム通信が必要ならECSを検討
- 永続ストレージなし — ファイルシステムはエフェメラル
DifyのSSE(Server-Sent Events)による応答ストリーミングはHTTPベースなので問題なく動作します。
設計の鉄則 — ステートレスとステートフルを分離する
「ステートレスなものはコンテナに、
ステートフルなものはマネージドサービスに」
なぜ分離が必要なのか
公式docker-composeではデータがDockerボリュームとして1台のサーバーに保存されます。オートスケールでコンテナが2台に増えた場合、新しいコンテナには前のデータがありません。コンテナ内にデータを持っている限り、スケールできないのです。
Difyの構成要素を分類する
| コンポーネント | 種別 | 配置先 |
|---|---|---|
| API Server(Flask/Gunicorn) | ステートレス | コンテナ内 |
| Celery Worker | ステートレス | コンテナ内 |
| Web UI(Next.js) | ステートレス | コンテナ内 |
| Plugin Daemon | ステートレス | コンテナ内 |
| nginx(リバースプロキシ) | ステートレス | コンテナ内 |
| PostgreSQL | ステートフル | → RDS |
| Redis | ステートフル | → ElastiCache |
| ファイルストレージ | ステートフル | → S3 |
| ベクトルDB(pgvector) | ステートフル | → RDS(同居) |
なぜマネージドサービスなのか
「EC2にPostgreSQLを自分でインストールすれば安い」という声もありますが、以下を自分でやる覚悟が必要です:
- 自動バックアップ — RDSなら設定一行。セルフなら
pg_dump+ cronをメンテナンス - セキュリティパッチ — RDSは自動適用。セルフなら手動
- フェイルオーバー — RDS Multi-AZなら自動切替。セルフならレプリケーション構築
- モニタリング — RDSはCloudWatch統合。セルフならPrometheus等を自前構築
開発環境だからこそ運用にかける時間を最小化したい。マネージドサービスの月額差額は、エンジニアの時間単価と比較すれば誤差です。
全体構成図
┌─────────────────────────────────────────────────────────────────┐ │ AWS Cloud (ap-northeast-1) │ │ │ │ ┌────────────────────────────────────────────────────────────┐ │ │ │ VPC (10.0.0.0/16) │ │ │ │ │ │ │ │ Public Subnets ─── [ NAT Gateway ] │ │ │ │ │ │ │ │ Private Subnets │ │ │ │ ┌────────────────────────────────────────────────────┐ │ │ │ │ │ App Runner (VPC Connector) │ │ │ │ │ │ │ │ │ │ │ │ ┌──────────────────────────────────────────┐ │ │ │ │ │ │ │ All-in-One コンテナ (:5001) │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ nginx (:5001) ─┬─ /api/ → API:5100 │ │ │ │ │ │ │ │ (supervisord) ├─ /console/ → API:5100 │ │ │ │ │ │ │ │ └─ /* → Web:3000 │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ [API:5100] [Worker] [Web:3000] [Plugin] │ │ │ │ │ │ │ │ gunicorn celery Next.js Daemon │ │ │ │ │ │ │ └──────────────────────────────────────────┘ │ │ │ │ │ └────────────────────────────────────────────────────┘ │ │ │ │ │ │ │ │ │ │ │ ▼ ▼ ▼ │ │ │ │ ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │ │ │ RDS │ │ElastiCache│ │ S3 │ │ │ │ │ │ PG16 │ │ Redis 7.1│ │(storage)│ │ │ │ │ │+pgvector│ │t4g.micro │ │ │ │ │ │ │ └─────────┘ └─────────┘ └─────────┘ │ │ │ └────────────────────────────────────────────────────────────┘ │ │ [ ECR ] ← Dockerイメージ保存 │ └─────────────────────────────────────────────────────────────────┘
月額コスト概算
| サービス | スペック | 月額概算 |
|---|---|---|
| App Runner | 1vCPU / 2GB RAM | ~$30 |
| RDS PostgreSQL | db.t4g.micro, 20GB gp3 | ~$15 |
| ElastiCache Redis | cache.t4g.micro | ~$12 |
| NAT Gateway | 1 AZ | ~$35 |
| S3 + ECR | 従量(少量) | ~$2 |
| 合計 | ~$94/月 | |
💡 コスト削減のヒント
最大のコスト要因はNAT Gateway(~$35/月)です。開発環境ではVPC Endpointやパブリックサブネット配置で回避する手もありますが、セキュリティとのトレードオフです。
Terraform モジュール構成
terraform/
├── modules/
│ ├── vpc/ # VPC, Subnet, NAT GW, Security Groups
│ ├── rds/ # PostgreSQL 16 + pgvector
│ ├── elasticache/ # Redis 7.1
│ ├── s3/ # ファイルストレージ
│ ├── ecr/ # コンテナレジストリ
│ ├── apprunner/ # App Runner + IAM + VPC Connector
│ └── ssm/ # パラメータストア
└── development/
├── main.tf
├── variables.tf
├── terraform.tfvars
├── providers.tf
└── outputs.tf
App Runner モジュール(核心部分)
resource "aws_apprunner_service" "main" {
service_name = "${var.project}-${var.environment}"
source_configuration {
image_repository {
image_identifier = "${var.ecr_repository_url}:latest"
image_repository_type = "ECR"
image_configuration {
port = "5001" # nginx がリッスンするポート
runtime_environment_variables = {
# Core
SECRET_KEY = random_password.secret_key.result
# Database (RDS)
DB_HOST = var.db_host
DB_USERNAME = var.db_username
DB_PASSWORD = var.db_password
DB_DATABASE = var.db_database
# Redis (ElastiCache)
REDIS_HOST = var.redis_host
REDIS_USE_SSL = "false"
# Celery Broker → Redis (NOT RabbitMQ!)
CELERY_BROKER_URL = "redis://${var.redis_host}:6379/1"
BROKER_USE_SSL = "false"
# S3
STORAGE_TYPE = "s3"
S3_BUCKET_NAME = var.s3_bucket_name
S3_REGION = var.aws_region
# Vector DB (pgvector on same RDS)
VECTOR_STORE = "pgvector"
PGVECTOR_HOST = var.db_host
PGVECTOR_USER = var.db_username
PGVECTOR_PASSWORD = var.db_password
# Plugin Daemon
PLUGIN_API_URL = "http://127.0.0.1:5002"
PLUGIN_API_KEY = random_password.secret_key.result
PLUGIN_DAEMON_KEY = random_password.secret_key.result
INNER_API_KEY_FOR_PLUGIN = random_password.secret_key.result
MARKETPLACE_ENABLED = "true"
}
}
}
auto_deployments_enabled = false
}
network_configuration {
egress_configuration {
egress_type = "VPC"
vpc_connector_arn = aws_apprunner_vpc_connector.main.arn
}
}
health_check_configuration {
protocol = "HTTP"
path = "/health"
interval = 10
}
}
All-in-One コンテナの設計
App Runnerは1サービスにつき1コンテナ。Difyは本来5つのプロセスが必要なので、supervisordで統合しました。
Dockerfile — マルチステージビルド
FROM langgenius/dify-api:latest AS api
FROM langgenius/dify-web:latest AS web
FROM langgenius/dify-plugin-daemon:0.5.3-local AS plugin-daemon
FROM langgenius/dify-api:latest
USER root
RUN apt-get update && \
apt-get install -y --no-install-recommends \
nginx supervisor postgresql-client && \
rm -rf /var/lib/apt/lists/*
# Web frontend
COPY --from=web /app /app-web
# Plugin daemon binary
COPY --from=plugin-daemon /app/main /app/plugin-daemon/main
COPY --from=plugin-daemon /app/.tiktoken /app/plugin-daemon/.tiktoken
RUN chmod +x /app/plugin-daemon/main && \
mkdir -p /app/plugin-daemon/storage/cwd
COPY nginx.conf /etc/nginx/sites-available/default
COPY supervisord.conf /etc/supervisor/conf.d/dify.conf
COPY init-db.sh start.sh run-plugin-daemon.sh /usr/local/bin/
RUN chmod +x /usr/local/bin/*.sh
ENTRYPOINT []
EXPOSE 5001
CMD ["/usr/local/bin/start.sh"]
💡 ポイント: ENTRYPOINT [] の上書き
dify-apiのベースイメージには/entrypoint.shがENTRYPOINTとして設定されています。CMDだけ変更してもENTRYPOINTが優先されるため、ENTRYPOINT []で明示的に空にする必要があります。
supervisord.conf — 5プロセス管理
[supervisord]
nodaemon=true
user=root
[program:api]
command=/bin/bash /entrypoint.sh
directory=/app/api
environment=MODE="api",DIFY_PORT="5100",MIGRATION_ENABLED="true"
autorestart=true
[program:worker]
command=/bin/bash /entrypoint.sh
directory=/app/api
environment=MODE="worker"
autorestart=true
startretries=10
[program:web]
command=node /app-web/web/server.js
directory=/app-web/web
environment=PORT="3000",HOSTNAME="0.0.0.0",
NEXT_PUBLIC_API_PREFIX="/console/api",
NEXT_PUBLIC_PUBLIC_API_PREFIX="/api",EDITION="SELF_HOSTED"
[program:plugin-daemon]
command=/usr/local/bin/run-plugin-daemon.sh
directory=/app/plugin-daemon
autorestart=true
startretries=30
[program:nginx]
command=nginx -g "daemon off;"
autorestart=true
nginx.conf — リバースプロキシ
server {
listen 5001;
client_max_body_size 15M;
# API routes → Flask/Gunicorn (:5100)
location /console/api/ {
proxy_pass http://127.0.0.1:5100/console/api/;
proxy_set_header Host $host;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
proxy_buffering off;
proxy_read_timeout 300s;
}
location /api/ { proxy_pass http://127.0.0.1:5100/api/; ... }
location /v1/ { proxy_pass http://127.0.0.1:5100/v1/; ... }
location /files/ { proxy_pass http://127.0.0.1:5100/files/; }
location /health { proxy_pass http://127.0.0.1:5100/health; }
# Everything else → Next.js (:3000)
location / {
proxy_pass http://127.0.0.1:3000;
proxy_set_header Host $host;
}
}
🔥 実際にハマった10のポイントと解決策
ここからが本番です。ドキュメントに書いてない、やってみないとわからないトラブルの数々を共有します。
❶ pgvectorのshared_preload_librariesエラー
🚨 エラー
"vector" is not an allowed value for shared_preload_libraries
pgvectorはPostgreSQLのEXTENSIONであり、shared_preload_librariesに登録する共有ライブラリではありません。
# ❌ 間違い
parameter {
name = "shared_preload_libraries"
value = "vector"
}
# ✅ 正解 — pgvectorはCREATE EXTENSIONで有効化するだけ
parameter {
name = "shared_preload_libraries"
value = "pg_stat_statements"
}
❷ nginx proxy_passのパス消失問題
🚨 症状
/console/api/setup にアクセスすると / にリダイレクトされる
# ❌ パスが消える — /console/api/setup → / として転送
location /console/api/ {
proxy_pass http://127.0.0.1:5100/;
}
# ✅ パスを保持 — /console/api/setup → /console/api/setup
location /console/api/ {
proxy_pass http://127.0.0.1:5100/console/api/;
}
❸ Next.jsのバインドアドレス問題
🚨 症状
502 Bad Gateway — Next.jsは動いているのにnginxから到達できない
Next.jsスタンドアロンモードはデフォルトでコンテナの内部IP(172.17.0.2等)にバインドされ、127.0.0.1:3000ではリッスンしていません。
# supervisord.conf で HOSTNAME を指定
[program:web]
environment=PORT="3000",HOSTNAME="0.0.0.0",...
❹ Plugin Daemonの未ドキュメント環境変数 PLATFORM
🚨 エラー
Config.Platform: Field validation failed on the 'required' tag
PLATFORM環境変数が必須だが公式ドキュメントに記載なし。公式docker-compose.yamlにPLATFORM=localがありました。
# run-plugin-daemon.sh
export PLATFORM="local"
exec /app/plugin-daemon/main
❺ RDSのSSL接続必須
🚨 エラー
no pg_hba.conf entry for host "10.0.11.225", no encryption
AWS RDSはデフォルトでSSL接続を要求。DB_SSL_MODE=disableではpg_hba.confレベルで拒否されます。
export DB_SSL_MODE="require" # ← "disable" ではダメ!
❻ supervisordの%(ENV_...)s展開が不安定
supervisord.confで%(ENV_PLUGIN_DAEMON_KEY)sのように環境変数を参照しようとしたが、展開されないケースがある。ラッパースクリプトでシェル変数展開するのが確実。
#!/bin/bash
# run-plugin-daemon.sh
export SERVER_KEY="${PLUGIN_DAEMON_KEY}"
export DIFY_INNER_API_KEY="${INNER_API_KEY_FOR_PLUGIN}"
export DB_SSL_MODE="require"
export PLATFORM="local"
exec /app/plugin-daemon/main
❼ dify_pluginデータベースの自動作成
Plugin Daemonはdify_pluginという別データベースを要求するが、RDSの初期状態にはない。コンテナ起動時に自動作成するスクリプトを追加。
#!/bin/bash
# init-db.sh
for i in $(seq 1 30); do
pg_isready -h "$DB_HOST" -U "$DB_USERNAME" 2>/dev/null && break
sleep 2
done
PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -U "$DB_USERNAME" \
-d "$DB_DATABASE" \
-c "SELECT 1 FROM pg_database WHERE datname='dify_plugin'" \
| grep -q 1 || \
PGPASSWORD="$DB_PASSWORD" psql -h "$DB_HOST" -U "$DB_USERNAME" \
-d "$DB_DATABASE" -c "CREATE DATABASE dify_plugin;"
⚠️ 重要:バックグラウンド実行にすること
#!/bin/bash
# start.sh — エントリーポイント
/usr/local/bin/init-db.sh & # バックグラウンド!
exec /usr/bin/supervisord -c /etc/supervisor/supervisord.conf
同期実行するとDB接続待ちでsupervisordの起動が遅延し、ヘルスチェックに失敗します。
❽ Plugin Daemonのlatestタグが存在しない
docker pull langgenius/dify-plugin-daemon:latestが404。公式docker-compose.yamlでバージョンを確認:
curl -s https://raw.githubusercontent.com/langgenius/dify/main/docker/docker-compose.yaml \
| grep plugin-daemon
# → langgenius/dify-plugin-daemon:0.5.3-local
❾ App Runner CREATE_FAILEDからの復旧
ECRにイメージがない状態でterraform applyするとCREATE_FAILEDに。この状態ではstart-deploymentができません。
# destroyして再作成するしかない
terraform destroy -target=module.apprunner.aws_apprunner_service.main
# ECRにイメージをpush後
terraform apply -target=module.apprunner.aws_apprunner_service.main
❿ Celery WorkerのRabbitMQデフォルト問題
🚨 エラー
Cannot connect to amqp://guest:**@127.0.0.1:5672//: Connection refused
Dify 1.xのCELERY_BROKER_URLのデフォルトがNoneで、Celeryのデフォルト(amqp://localhost)にフォールバックします。明示的にRedisを指定:
CELERY_BROKER_URL = "redis://${var.redis_host}:6379/1"
BROKER_USE_SSL = "false"
デプロイ手順まとめ
# 1. Terraform で AWS リソース作成(約15分)
cd terraform/development
terraform init && terraform apply
# 2. Docker イメージビルド
cd ../../docker
docker build -t dify-allinone:latest .
# 3. ECR にプッシュ
aws ecr get-login-password --region ap-northeast-1 | \
docker login --username AWS --password-stdin $ECR_URL
docker tag dify-allinone:latest $ECR_URL/dify-development:latest
docker push $ECR_URL/dify-development:latest
# 4. デプロイ
aws apprunner start-deployment --service-arn $SERVICE_ARN
# 5. 確認
curl https://$APP_RUNNER_URL/health
# → {"status": "ok", "version": "1.13.0"}
まとめ
- ステートレスとステートフルの分離 — アプリはコンテナに、データ層はAWSマネージドに
- App Runnerで運用負荷を最小化 — ALB不要、HTTPS自動、オートスケール組み込み
- Terraformで全インフラをコード管理 — 再現性と環境間の一貫性を確保
- supervisordによるAll-in-Oneコンテナ — App Runnerの1コンテナ制約をクリア
- 月額~$94 — 開発環境として十分に低コスト
ソースコードはGitHubで公開しています。
📝 教訓10選(再掲)
- pgvectorはshared_preload_libraries不要
- nginx proxy_passの末尾/に注意
- Next.jsはHOSTNAME=0.0.0.0必須
- Plugin DaemonのPLATFORM環境変数は必須(ドキュメントなし)
- RDSはSSL必須 — DB_SSL_MODE=require
- supervisordの%(ENV_...)sは不安定 — ラッパースクリプトで
- dify_pluginデータベースは自動作成(バックグラウンドで)
- dify-plugin-daemonにlatestタグなし
- App Runner CREATE_FAILEDは再デプロイ不可
- CeleryブローカーはデフォルトRabbitMQ — 明示的にRedis指定
