社内で生成AIを使ったアプリを作りたいとなったとき、選択肢のひとつになるのがDifyです。オープンソースで、セルフホストできるため、社内データを外部のサービスに預けずに済むのが大きな利点です。
ただ、Difyは15個ものコンテナで構成されていて、公式の手順どおりに進めても環境ごとに引っかかる箇所が出てきます。そこで本記事では、EC2の起動からDocker・Docker Composeの導入、Difyの起動までをCloudFormationのテンプレート1本で完結させる構成を紹介します。作っては壊せるようにしておくと、検証も後片付けも楽になります。
なお本記事の手順は、東京リージョンで実際にデプロイして動作を確認しています。所要時間はスタック作成開始からCREATE_COMPLETEまで約7分でした。
1. どういう構成を作るのか

パブリックサブネットにEC2を1台立て、その上でDocker ComposeによりDifyを動かすシンプルな構成です。検証用途を想定しているため、RDSやElastiCacheには分離していません。
インスタンスタイプはt3.large(8GB)にしました。実際に起動した直後のメモリ使用量は約2.6GBだったので、t3.medium(4GB)でも動きますが余裕はほとんどありません。ナレッジを登録して使い込むことを考えると、8GBは見ておいたほうが安全です。
設計上のポイントは2つあります。
SSHの鍵を使わない:22番ポートを開けず、AWS Systems Manager Session Manager経由でログインします。鍵ファイルの管理が不要になり、鍵の紛失や共有といった悩みがなくなります。
Webの公開範囲を絞る:80番は自分のグローバルIPからのみ許可します。Difyは初回アクセスした人が管理者になる仕様なので、構築直後に全開放しておくのは危険です。
2. なぜCloudFormationにするのか
手順書として「EC2を立てて、Dockerを入れて、docker composeを叩いて…」と書くこともできますが、次の点でテンプレート化したほうが結果的に早くなります。
- 検証で作って壊してを繰り返しても、毎回同じ環境が手に入る
- 消し忘れによる課金を防げる。スタックを削除すれば関連リソースがまとめて消える
- インスタンスタイプや許可IPをパラメータにしておけば、本番と検証で使い回せる
3. テンプレート
以下が全体です。長いですが、やっていることは「IAMロール」「セキュリティグループ」「EC2とUserData」の3つだけです。
AWSTemplateFormatVersion: "2010-09-09"
Description: Dify single instance with Docker Compose
Parameters:
VpcId:
Type: AWS::EC2::VPC::Id
SubnetId:
Type: AWS::EC2::Subnet::Id
AllowedCidr:
Type: String
Description: 管理画面へのアクセスを許可するCIDR
AllowedPattern: ^(\d{1,3}\.){3}\d{1,3}/\d{1,2}$
InstanceType:
Type: String
Default: t3.large
AllowedValues: [t3.medium, t3.large, t3.xlarge]
VolumeSize:
Type: Number
Default: 30
MinValue: 20
LatestAmiId:
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64
Resources:
# Session Manager でログインするためのロール
InstanceRole:
Type: AWS::IAM::Role
Properties:
AssumeRolePolicyDocument:
Version: "2012-10-17"
Statement:
- Effect: Allow
Principal:
Service: ec2.amazonaws.com
Action: sts:AssumeRole
ManagedPolicyArns:
- arn:aws:iam::aws:policy/AmazonSSMManagedInstanceCore
InstanceProfile:
Type: AWS::IAM::InstanceProfile
Properties:
Roles:
- !Ref InstanceRole
SecurityGroup:
Type: AWS::EC2::SecurityGroup
Properties:
GroupDescription: Dify web UI
VpcId: !Ref VpcId
SecurityGroupIngress:
- IpProtocol: tcp
FromPort: 80
ToPort: 80
CidrIp: !Ref AllowedCidr
Instance:
Type: AWS::EC2::Instance
CreationPolicy:
# UserData が完走するまで CREATE_COMPLETE にしない
ResourceSignal:
Timeout: PT25M
Count: 1
Properties:
ImageId: !Ref LatestAmiId
InstanceType: !Ref InstanceType
IamInstanceProfile: !Ref InstanceProfile
SubnetId: !Ref SubnetId
SecurityGroupIds:
- !Ref SecurityGroup
BlockDeviceMappings:
- DeviceName: /dev/xvda
Ebs:
VolumeSize: !Ref VolumeSize
VolumeType: gp3
DeleteOnTermination: true
UserData:
Fn::Base64: !Sub |
#!/bin/bash
set -xe
exec > >(tee -a /var/log/dify-setup.log) 2>&1
dnf install -y aws-cfn-bootstrap docker git
systemctl enable --now docker
usermod -aG docker ec2-user
# Docker Compose v2 は AL2023 の標準リポジトリに無いので直接配置する
DOCKER_CONFIG=/usr/libexec/docker/cli-plugins
mkdir -p $DOCKER_CONFIG
curl -sSL https://github.com/docker/compose/releases/download/v2.29.7/docker-compose-linux-x86_64 \
-o $DOCKER_CONFIG/docker-compose
chmod +x $DOCKER_CONFIG/docker-compose
# Dify を取得して起動する
git clone --depth 1 https://github.com/langgenius/dify.git /opt/dify
cd /opt/dify/docker
cp .env.example .env
docker compose up -d
# 応答するまで待ってから完了を通知する
for i in $(seq 1 60); do
if curl -fsS http://localhost/ >/dev/null 2>&1; then
break
fi
sleep 10
done
/opt/aws/bin/cfn-signal -e $? --stack ${AWS::StackName} \
--resource Instance --region ${AWS::Region}
Outputs:
DifyUrl:
Value: !Sub http://${Instance.PublicIp}/install
SessionManagerCommand:
Value: !Sub aws ssm start-session --target ${Instance} --region ${AWS::Region}
4. テンプレートの要点
4.1 AMIをハードコードしない
LatestAmiIdはSSMのパブリックパラメータを参照しています。AMI IDはリージョンごとに違ううえ、更新されるたびに変わります。ここを直書きすると、他のリージョンで動かなくなったり、古いAMIを使い続けることになります。
LatestAmiId:
Type: AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>
Default: /aws/service/ami-amazon-linux-latest/al2023-ami-kernel-default-x86_64
この書き方にしておくと、デプロイのたびに最新のAmazon Linux 2023が使われます。
4.2 Docker Compose v2の導入
Amazon Linux 2023の標準リポジトリにはDocker Composeのプラグインが入っていません。dnf install docker-compose-pluginは通らないので、バイナリを直接配置します。
DOCKER_CONFIG=/usr/libexec/docker/cli-plugins
mkdir -p $DOCKER_CONFIG
curl -sSL https://github.com/docker/compose/releases/download/v2.29.7/docker-compose-linux-x86_64 \
-o $DOCKER_CONFIG/docker-compose
chmod +x $DOCKER_CONFIG/docker-compose
/usr/libexec/docker/cli-pluginsに置くことで、docker compose(ハイフンなし)のサブコマンドとして認識されます。
4.3 cfn-signalで「動いている」ことまで保証する
ここが今回いちばん伝えたい部分です。

UserDataは非同期に実行されるため、何もしないとEC2が起動した時点でスタックはCREATE_COMPLETEになります。しかしDifyのコンテナはまだイメージを取得している最中で、URLを開いてもつながりません。
CreationPolicyとcfn-signalを組み合わせると、UserDataが最後まで通ったことを確認してからスタックを完了扱いにできます。
CreationPolicy:
ResourceSignal:
Timeout: PT25M
Count: 1
さらにUserDataの末尾で、実際に80番が応答するまで待ってから通知しています。これで「スタックはできたのにアプリが動いていない」という状態を避けられます。
タイムアウトは余裕を見て25分にしていますが、実測ではEC2の作成開始からCREATE_COMPLETEまで約4分15秒、スタック全体でも約7分で完了しました。大半はコンテナイメージの取得時間です。
5. デプロイする
自分のグローバルIPを調べてから実行します。
# 自分のグローバルIPを確認
curl -s https://checkip.amazonaws.com
# デプロイ(VPC IDとサブネットIDは自分の環境のものに置き換える)
aws cloudformation deploy \
--template-file dify-cloudformation.yaml \
--stack-name dify \
--capabilities CAPABILITY_IAM \
--region ap-northeast-1 \
--parameter-overrides \
VpcId=vpc-xxxxxxxx \
SubnetId=subnet-xxxxxxxx \
AllowedCidr=203.0.113.10/32 \
InstanceType=t3.large
CAPABILITY_IAMは、テンプレートがIAMロールを作るため必要です。付けずに実行すると弾かれます。
6. 動作確認
デプロイが完了したら、URLを取り出してブラウザで開きます。
aws cloudformation describe-stacks --stack-name dify \
--region ap-northeast-1 \
--query "Stacks[0].Outputs" --output table
DifyUrlに表示されたURL(http://<パブリックIP>/install)が初期設定画面です。最初にアクセスした人が管理者になるので、デプロイしたら早めに初期設定を済ませてください。
なお、EC2を停止して再度起動するとパブリックIPが変わります。検証を中断して再開する運用をするなら、Elastic IPの割り当てを検討してください。
コンテナの起動状況も確認しておきます。実際に動いていたのは以下の15個でした。
agent_backend Up 2 minutes
agent_ssrf_proxy Up 3 minutes
api Up 2 minutes (healthy)
api_websocket Up 2 minutes
db_postgres Up 3 minutes (healthy)
local_sandbox Up 2 minutes (healthy)
nginx Up 2 minutes
plugin_daemon Up 2 minutes
redis Up 2 minutes (healthy)
sandbox Up 3 minutes (healthy)
ssrf_proxy Up 2 minutes
weaviate Up 3 minutes
web Up 2 minutes
worker Up 2 minutes
worker_beat Up 2 minutes
構築がうまくいかない場合は、Session Managerでログインしてログを見ます。
# SSH鍵は不要
aws ssm start-session --target i-xxxxxxxxxxxx --region ap-northeast-1
# UserData の実行ログ
sudo tail -100 /var/log/dify-setup.log
# コンテナの状態
cd /opt/dify/docker && sudo docker compose ps
7. 後片付け
検証が終わったらスタックごと削除します。EC2もEBSもセキュリティグループもまとめて消えるので、消し忘れによる課金を防げます。
aws cloudformation delete-stack --stack-name dify --region ap-northeast-1
aws cloudformation wait stack-delete-complete --stack-name dify --region ap-northeast-1
8. まとめ
Difyのような複数コンテナで動くアプリは、手順書で追いかけるより、テンプレートにまとめてしまったほうが確実です。要点を整理しておきます。
- AMIはSSMパブリックパラメータで参照し、直書きしない
- Amazon Linux 2023にはDocker Composeプラグインが無いので、バイナリを直接配置する
CreationPolicyとcfn-signalで、アプリが起動したことまで確認してから完了扱いにする- SSH鍵ではなくSession Managerを使い、22番ポートを開けない
- Webの公開範囲は自分のIPに絞る。Difyは先着で管理者が決まる
なお本記事の構成は検証用です。本番運用するのであれば、データベースをRDSに、ファイルストレージをS3に分離し、ALB+ACMでHTTPS化する構成を検討してください。
DifyがどういうツールなのかについてはDifyとは何か——セルフホストで始める生成AIアプリ開発で整理しています。DockerのインストールそのものについてはAlmaLinuxにDockerとDocker Composeをインストールする方法でも解説しています。
次回は、ここで構築した環境のモデルプロバイダーとしてAWS Bedrockを使えるようにします。まずはAWS側の準備(Anthropicモデルのユースケース申請と推論プロファイル)から解説します。


コメント