DifyのストレージをAmazon S3に切り替える設定手順と.envの優先順位
セルフホスト版のDifyは、アップロードしたファイルを既定でコンテナ内のローカルディスクに保存します。これをAmazon S3へ切り替える作業自体は環境変数の書き換えだけです。ただしDify 1.14.1(2026年5月12日公開)で設定ファイルの構成が変わり、S3の項目が docker/.env.example から消えました。書き場所が分かりにくいうえ、新しい置き場所へ正しく書いても docker/.env の値に打ち消されます。この記事では公式リポジトリの実装とリリースタグを照合し、現行の1.16.1で確実に通る手順を整理します。
まとめ:設定ファイルの優先順位とキー名の対応
変数名は昔から変わっていません。変わったのは定義ファイルの構成と読み込み順です。
| 項目 | 1.14.0 以前 | 1.14.1 以降(1.16.1 で確認) |
|---|---|---|
| S3設定の雛形 | docker/.env.example | docker/envs/core-services/shared.env.example |
| 読み込み方式 | compose の変数展開 | env_file(envs配下→最後に .env) |
| 優先されるファイル | docker/.env | docker/.env(後勝ちで最優先) |
| 切り替えキー | STORAGE_TYPE=s3 | STORAGE_TYPE=s3(変更なし) |
| 認証キー名 | S3_ACCESS_KEY/S3_SECRET_KEY | 同じ |
| docker/.env 欠如時 | 既定値のまま起動 | Compose がエラーで起動しない |
| shared.env 欠如時 | 該当ファイルなし | 無視して既定のローカル保存で起動 |
最短の手順は、docker/.env に STORAGE_TYPE=s3 と S3_* を書くことです。最優先で読まれるファイルなので、他のどこに何が書かれていても勝ちます(environment: だけは env_file より強いものの、STORAGE_TYPE と S3_* はapi・workerのどちらにも書かれていません)。設定を分割したいなら envs/core-services/shared.env も使えます。そのときは .env 側に STORAGE_TYPE=opendal が残っていないかの確認が必須です。
S3へ保存される対象と既定のopendal構成
STORAGE_TYPE が制御するのは、ナレッジベースに取り込んだ元ファイル、チャットでアップロードされた画像や文書、ワークフローが生成したファイルといった「ファイル実体」の保存先です。アプリの設定やワークフロー定義、会話履歴はPostgreSQLに、ベクトルは VECTOR_STORE で指定したベクトルデータベースに入るため、S3へ移しても構成は変わりません。
Dify 1.16.1 の既定値は STORAGE_TYPE=opendal、OPENDAL_SCHEME=fs、OPENDAL_FS_ROOT=storage です。ファイルはコンテナ内の /app/api/storage に書かれ、composeのボリューム定義で ./volumes/app/storage にマウントされます。単一ホストなら支障はありません。S3へ移す実質的な理由は、apiとworkerを別ノードに分ける構成やEKSで複数レプリカを走らせる構成で、書いたノードと読むノードが一致しなくなることにあります。
STORAGE_TYPE が受け付ける値は、ソース上の _VALID_STORAGE_TYPE に列挙された13種です。opendal と s3 のほか各社のオブジェクトストレージ、そして local が並びます。この local は説明文で (deprecated) local と明記され、指定しても opendal の fs スキームへ委譲されるだけなので、新規に選ぶ値ではありません。
1.14.1で変わった設定ファイルの構成と.envの優先順位
ウェブ上に残るDifyのS3設定手順は「docker/.env に S3_* を追記する」と書いており、この手順自体は今も有効に働きます。つまずくのは、公式の雛形からS3の項目が消えて書き場所が分からなくなり、新しい置き場所を使うと優先順位の罠にはまる点です。
1.14.0以前:composeが.envの値を変数展開する構成
1.14.0 のタグでは docker/.env.example の477行目以降に S3_* の7項目が並んでいました。当時の docker-compose.yaml には env_file が1つも無く、${S3_ENDPOINT:-} のような変数展開で .env の値を各コンテナへ流し込む方式です。設定先が1ファイルだけなので、迷う余地はありませんでした。
1.14.1以降:envs配下を読んだあと.envを最後に読む構成
1.14.1 で docker/envs/ ディレクトリが新設され、.env.example から S3_* が消えました。1.16.1 の docker/.env.example を検索しても S3_ は1行も出てきません。現在の定義先は docker/envs/core-services/shared.env.example で、1.16.1 時点では3箇所に散っています。121行目付近が S3_ACCESS_KEY と S3_SECRET_KEY、309行目付近が S3_ENDPOINT ほか5つ、451行目が STORAGE_TYPE です。最初に当たった箇所だけ直すと認証情報しか入らないので、S3_ で全件を確認してください。行番号は次のリリースでずれます。
読み込み方式も変わりました。apiコンテナは x-shared-api-worker-config、workerコンテナは x-shared-worker-config という別々のアンカーを使いますが、構造は同じです。envs/core-services/shared.env をはじめ多数のファイルを env_file で読み、これらはすべて required: false で存在しなくてもエラーになりません。そして両方とも、リストの最後に - ./.env が置かれています。こちらは required の指定が無く、Compose仕様の既定値 true が適用されます。
Compose は env_file のリストを上から順に処理し、同じ変数が複数のファイルにあれば後のファイルの値が残ります。ファイル単位の丸ごと置換ではなく、変数単位のマージです。.env がリストの最後にある以上、.env の値が最優先になります。Dify自身も docker/.env.example の冒頭コメントで「Values in docker/.env take precedence over docker/envs/*.env files.」と明記しています。
ここに罠があります。公式の導入手順は cp .env.example .env から始まり、その .env.example の167行目にあるのが STORAGE_TYPE=opendal です。つまり標準的な手順で立てた環境の .env には、ほぼ確実にこの行が入っています。この状態で envs/core-services/shared.env に STORAGE_TYPE=s3 と一式を書いても、最後に読まれる .env の opendal が勝ち、保存先はローカルのままです。S3_* の値は正しく渡っているのに切り替えだけが効かないため、認証やIAM権限を疑って時間を溶かすことになります。
回避策は2つ。設定を .env にまとめて書くか、shared.env を使うなら .env 側の STORAGE_TYPE=opendal を削除するか s3 に書き換えることです。分割管理が要らなければ前者を勧めます。読み込み順を意識せずに済み、確認も1ファイルで完結します。
S3バケットとIAMポリシーの準備
Difyが実行するS3操作から逆算した最小権限
付与すべき権限は、実装が呼んでいるAPIから確定できます。aws_s3_storage.py が使うのは head_bucket、create_bucket、put_object、get_object、head_object、download_file、delete_object、generate_presigned_url です。download_file は内部的に GetObject、generate_presigned_url は署名生成のみで追加権限が要りません。バケットは後述のとおり事前作成するので s3:CreateBucket も不要です。
{
"Version": "2012-10-17",
"Statement": [
{
"Sid": "DifyObjectAccess",
"Effect": "Allow",
"Action": [
"s3:GetObject",
"s3:PutObject",
"s3:DeleteObject"
],
"Resource": "arn:aws:s3:::dify-storage-example/*"
},
{
"Sid": "DifyBucketCheck",
"Effect": "Allow",
"Action": [
"s3:ListBucket"
],
"Resource": "arn:aws:s3:::dify-storage-example"
}
]
}
2つ目のステートメントを忘れやすいので補足します。起動時の head_bucket はバケット自体への操作で、AWSのAPIリファレンスも必要権限を s3:ListBucket と定めています。オブジェクト単位の arn:aws:s3:::bucket/* では許可されないため、バケットARNを対象にした指定が別に要るのです。オブジェクト権限だけの状態は、後述する403の握りつぶしと相まって症状が分かりにくくなります。
バケットをSSE-KMSで暗号化している場合は、使用するKMSキーに対する kms:GenerateDataKey と kms:Decrypt も必要です。S3のマネージドキー(SSE-S3)なら追加の権限は要りません。
AmazonS3FullAccessを避ける判断基準
手順を短くするために AmazonS3FullAccess を勧める記事がありますが、本番環境では使わないでください。このAWS管理ポリシーはアカウント内の全バケットに対する全操作を許可します。認証情報が漏れた場合、影響範囲がDify用バケットに閉じるか、同一アカウントのバックアップバケットや監査ログバケットまで及ぶかは、このポリシー選択で決まります。
検証環境を数時間だけ立てて壊す用途なら AmazonS3FullAccess で構いません。分かれ目は「その認証情報が本番データと同じAWSアカウントにあるか」です。同じアカウントなら上のカスタムポリシーを使ってください。
環境変数の記述パターン別の設定値
アクセスキー方式:AWS外からDifyを動かす場合
オンプレミスや他社クラウドでDifyを動かしている場合は、IAMユーザのアクセスキーを使います。優先順位の都合から、docker/.env に書く前提で示します。
STORAGE_TYPE=s3
S3_REGION=ap-northeast-1
S3_BUCKET_NAME=dify-storage-example
S3_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE
S3_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLE
S3_ADDRESS_STYLE=auto
S3_USE_AWS_MANAGED_IAM=false
S3_ENDPOINT は省略しています。AWSのS3を使うだけなら未設定でよく、boto3が S3_REGION から標準のエンドポイントを解決します。指定が要るのは、次に説明するS3互換ストレージの場合です。
雛形の shared.env.example をコピーして使うなら注意が要ります。同ファイルの S3_REGION=us-east-1 と S3_BUCKET_NAME=difyai は雛形の値であって、コード上の既定値ではありません。設定クラスではどちらも未設定が既定なので、ファイルを自作して S3_BUCKET_NAME を書き忘れると、バケット名が空のまま起動してboto3側でパラメータエラーになります。設定後は docker compose restart ではなく docker compose up -d でコンテナを作り直してください。
EC2・EKSでのIAMロール方式:アクセスキーを置かない構成
DifyをAWS上で動かしているなら、アクセスキーは使わずIAMロールに寄せてください。S3_USE_AWS_MANAGED_IAM=true にすると、実装は boto3.Session() を素で生成し、EC2のインスタンスプロファイルやEKSのIRSAから認証情報を解決します。
STORAGE_TYPE=s3
S3_USE_AWS_MANAGED_IAM=true
S3_REGION=ap-northeast-1
S3_BUCKET_NAME=dify-storage-example
この分岐では S3_ACCESS_KEY、S3_SECRET_KEY、S3_ENDPOINT、S3_ADDRESS_STYLE は参照されません。ソース上、IAMロール側のクライアント生成に渡されるのは region_name だけです。アクセスキー方式から移行したときに古い変数が残っていても動作には影響しませんが、消しておくほうが無難です。EC2への構築手順はDifyをEC2上に構築するための基本手順と事前準備、EKS構成はAWS EKSにDifyを導入するための事前準備と環境構築のポイントで扱っています。
MinIOなどS3互換ストレージでの指定
MinIOやCloudflare R2のようなS3互換ストレージも STORAGE_TYPE=s3 で使えます。分かれ目は S3_ADDRESS_STYLE です。この値は auto、virtual、path の3つを受け付け、boto3クライアントの addressing_style にそのまま渡されます。
STORAGE_TYPE=s3
S3_ENDPOINT=http://minio:9000
S3_REGION=us-east-1
S3_BUCKET_NAME=dify
S3_ACCESS_KEY=minioadmin
S3_SECRET_KEY=minioadmin
S3_ADDRESS_STYLE=path
MinIOは既定でパス形式(http://host/bucket/key)のURLを使うため path を指定します。仮想ホスト形式は bucket.host の名前解決を前提とするので、コンテナ名でMinIOを参照する構成では失敗します。auto のままDNSエラーや接続タイムアウトが出るなら、まずここを疑ってください。
切り替え後の確認と既存ファイルの扱い
再起動したら、Difyのナレッジベースにテスト用のファイルを1つアップロードし、S3側にオブジェクトが増えたかをマネジメントコンソールで確認します。upload_files/ のようなプレフィックス配下にオブジェクトが作られていれば切り替えは成功です。Dify側の画面は保存先がローカルでも同じように成功を返すので、必ずS3側を見てください。
バケットは事前に作成してください。実装は起動時に head_bucket を実行し、404が返ると create_bucket を呼びますが、CreateBucketConfiguration を渡していません。AWSのCLIリファレンスは「us-east-1 以外のリージョンでは、目的のリージョンに作成するために適切な LocationConstraint の指定が必要」と定めているので、S3_REGION=ap-northeast-1 のような構成では自動作成に頼れません。自動作成されたバケットにはバージョニングも暗号化もライフサイクルルールも付きません。
aws s3api create-bucket --bucket dify-storage-example --region ap-northeast-1 --create-bucket-configuration LocationConstraint=ap-northeast-1
既存ファイルは移行されません。切り替え前のファイルは ./volumes/app/storage に残ったままで、Difyは新しい保存先だけを見にいきます。過去のナレッジのドキュメントを引き続き参照するなら、切り替え前に aws s3 sync でローカルの内容をバケットへ同期し、プレフィックス構造を維持してください。
プラグインデーモンのストレージが別系統である点
「Difyのファイルをすべて S3 へ寄せた」と考えていると食い違うポイントがあります。ここまでの STORAGE_TYPE と S3_* はapiとworkerが使うファイルストレージの設定で、プラグインデーモンは別の変数群を持つからです。docker-compose.yaml で S3_ を検索すると4件ヒットしますが、いずれもプラグインデーモンの environment: にあり、S3_USE_AWS_MANAGED_IAM: ${PLUGIN_S3_USE_AWS_MANAGED_IAM:-false} のようにコンテナ側は S3_*、.env 側は PLUGIN_S3_* という対応です。プラグインデーモンも末尾で .env を読みますが、environment: が env_file より優先されるため、.env のファイルストレージ用 S3_ENDPOINT がプラグイン側へ漏れることはありません。
PLUGIN_STORAGE_TYPE の既定は local、保存先は PLUGIN_STORAGE_LOCAL_ROOT=/app/storage で、インストール済みプラグインのパッケージがここに置かれます。冗長化する構成でなければローカルのままで支障はないものの、apiのストレージをS3へ移してもプラグイン側は移りません。ノードを入れ替えるとプラグインの再インストールが必要になる場合があります。プラグイン導入時のトラブルはDifyのプラグインがインストールできない原因と対処法【署名検証・容量・バージョン】にまとめています。
保存に失敗するときの切り分け手順
症状が出る順に見ていきます。複数のファイルに同じ変数が散らばる構成になったため、「どこに書いたか」ではなく「コンテナに何が渡ったか」から始めるのが確実です。
docker compose exec api env | grep -E "STORAGE_TYPE|S3_"
ここで STORAGE_TYPE=opendal が返ってくるなら、shared.env に s3 と書いていても値に負けています。まず docker/.env の STORAGE_TYPE の行を確認してください。それでも解決しなければ shared.env 自身の451行付近です。雛形ではS3の設定ブロックから140行ほど離れており、S3_ で検索しても引っかからないため、opendal のまま取り残されがちです。変数がまったく出てこない場合は、書いたファイルが読み込み対象になっていないか、.example を外し忘れています。
次に権限です。ここには実装由来の落とし穴があります。起動時の head_bucket が403(アクセス拒否)を返した場合、実装はエラーを投げず pass します。バケットが存在して権限だけ足りない可能性を考慮し、起動を止めない設計です。そのためIAMポリシーが不足していてもDifyは正常に起動し、ユーザーが実際にファイルをアップロードした瞬間に put_object が失敗します。起動が成功したことは権限が足りている証拠になりません。ログが静かでもアップロードが失敗するなら、真っ先に権限を疑ってください。
アップロードは通るのに読み出しで失敗する場合は、ログの NoSuchKey を見ます。実装は get_object の NoSuchKey を FileNotFoundError に変換するため、Dify側では「ファイルが見つかりません」として現れます。大半は切り替え前のファイルを参照しているケースで、原因は権限ではなく前章の未移行です。
同一ファイルへ短時間に連続アクセスしたときのエラーは別物です。ストリーム読み出しの分岐に、boto3の reached max retries を検知して「同じファイルを頻繁にリクエストしないでください」という例外へ変換する処理が入っています。ワークフローのループノードで同じファイルを繰り返し読んでいないか確認してください。
よくある質問
docker/.env に STORAGE_TYPE=opendal が残っている可能性が高いです。env_file は envs/*.env を読んだあと最後に .env を読むため、同じ変数があれば .env が勝ちます。公式の導入手順が cp .env.example .env から始まり、その雛形に該当行が含まれるためです。docker compose exec api env | grep STORAGE_TYPE で実際の値を確認してください。
MinIOなどのS3互換ストレージでも使えますか?
使えます。STORAGE_TYPE=s3 のまま S3_ENDPOINT に互換ストレージのエンドポイントを指定し、あわせて S3_ADDRESS_STYLE を設定してください。MinIOのようにパス形式のURLを使う実装なら path です。詳細は本文の「MinIOなどS3互換ストレージでの指定」を参照してください。
STORAGE_TYPEをopendalのままS3を使うことはできますか?
できますが、変数名の系統が変わります。STORAGE_TYPE=opendal と OPENDAL_SCHEME=s3 を指定すると、Difyは OPENDAL_S3_ で始まる環境変数を集め、プレフィックスを除いた小文字のキー名をopendalのOperatorへ渡します。この経路では S3_BUCKET_NAME などboto3実装向けの変数は参照されません。AWSのS3を使うだけなら STORAGE_TYPE=s3 のほうが扱いやすいです。
S3へ切り替えると、それまでにアップロードしたファイルはどうなりますか?
自動では移行されません。切り替え前のファイルは ./volumes/app/storage 配下に残り、Difyは新しい保存先だけを参照するため、既存のドキュメントを開くと「ファイルが見つかりません」となります。切り替え前に aws s3 sync で同じプレフィックス構造のままバケットへ同期しておいてください。
S3バケットは事前に作成しておく必要がありますか?
事実上必須です。実装はバケットが無ければ create_bucket を呼びますが、リージョンを指定するパラメータを渡さないため、us-east-1以外では自動作成が成立しません。本文の「切り替え後の確認と既存ファイルの扱い」に掲載したCLIコマンドで事前に作成し、IAMポリシーからは s3:CreateBucket を外す運用を勧めます。