Dify

DifyのストレージをAmazon S3に切り替える設定手順:.envの優先順位と既存ファイル・鍵の移行

DifyのストレージをAmazon S3に切り替える設定手順:.envの優先順位と既存ファイル・鍵の移行

セルフホスト版のDifyは、アップロードされたファイルを既定でホストのディスク(docker/volumes/app/storage)に保存します。保存先をAmazon S3に切り替える設定は環境変数の書き換えで済みますが、Dify 1.14.1で設定ファイルが docker/envs/ 配下に分割されてから、書いたはずの値が効かない事故が起きやすくなりました。同じ保存先には、自動生成された SECRET_KEY と、モデルプロバイダーのAPIキーを復号するRSA秘密鍵も置かれます。空のS3へ切り替えると、SECRET_KEY を自動生成に任せている環境ではログイン状態に影響し、秘密鍵を移さなければ登録済みのAPIキーを復号できません。この記事では最新の1.18.0(2026年10月23日公開)のソースと docker compose config による解決結果をもとに、効く書き場所と移行の順番を示します。

まとめ:S3切り替えで書く場所と移すもの

結論から書くと、STORAGE_TYPE=s3 は docker/.env に、S3の認証情報は docker/envs/core-services/shared.env に、プラグインデーモン用の PLUGIN_* は .env に書きます。PLUGIN_* は envs 配下に書いても反映されません。

設定 書くファイル 理由
STORAGE_TYPE=s3 docker/.env .env の opendal が最後に読まれて勝つ
S3_BUCKET_NAME・S3_REGION shared.env api・api_websocket・worker・worker_beat・plugin_daemon に渡る
S3_ACCESS_KEY・S3_SECRET_KEY shared.env .env だと web・sandbox にも渡る
SECRET_KEY(固定値) docker/.env 空欄だと保存先に自動生成される
PLUGIN_STORAGE_TYPE・PLUGIN_S3_* docker/.env envs 配下の値は変数展開で読まれない

運用中の環境は「SECRET_KEY を固定→S3の設定→docker compose up -d→flask migrate-oss で既存ファイルを移す→S3側で確認」の順で切り替えます。移行コマンドは秘密鍵の privkeys を運びますが、自動生成された SECRET_KEY のファイルは運ばないので、最初に固定します。

S3に保存されるファイルと保存されないデータ

STORAGE_TYPE が切り替えるのは「ファイル実体」の置き場です。ナレッジの元文書(upload_files)、チャットの添付画像、ツールの生成ファイル(tools)などです。アプリ定義や会話履歴はPostgreSQL、ベクトルは VECTOR_STORE のデータベースに残ります。見落とされやすいのは、ファイル以外の2つです。

  • ワークスペースごとの秘密鍵:KEY_PROVIDER_TYPE=local(既定)では、モデルプロバイダーのAPIキーやツールの認証情報を暗号化するRSA鍵の秘密鍵が privkeys/<tenant_id>/private.pem として保存先に書かれます(公式の環境変数リファレンスにも「keeps the private key in the STORAGE_TYPE backend」とあります)。
  • 自動生成の SECRET_KEY:1.14.1以降の .env.example は SECRET_KEY= が空欄で、コメントは「Leave empty to auto-generate a persistent key in the storage directory.」です。実装(api/configs/secret_key.py)は保存先から .dify_secret_key を読み、無ければ新しい鍵を生成して書き込みます。

保存先を空のS3に変えると、秘密鍵が見つからず PrivkeyNotFoundError になり、SECRET_KEY は別の値で作り直されます。SECRET_KEY はファイルの署名付きURLやツールのOAuthクライアント設定の暗号化にも使われています。

既定は STORAGE_TYPE=opendal・OPENDAL_SCHEME=fs で、単一ホストならこれで困りません。S3へ移す理由になるのは、EBSの容量が足りなくなったとき(Amazon EBSのボリュームタイプと料金)と、apiとworkerを複数ノードやEKSのレプリカに分けるときです。

1.14.1以降の設定ファイル構成と.envの優先順位

envs配下を読んだあと.envを最後に読む読み込み順

1.14.0までは docker/.env.example に S3_* の7項目がありました。1.14.1で docker/envs/ が新設され、S3の項目は envs/core-services/shared.env.example へ移り、1.18.0の .env.example には S3_ で始まる行がありません。

api・worker・worker_beat・plugin_daemon は env_file で shared.env などを読み(いずれも required: false)、リストの最後に ./.env を置いています。Composeは同じ変数が複数ファイルにあれば後のファイルの値を採るため、.env が最優先です。.env.example の冒頭にも「Values in docker/.env take precedence over docker/envs/*.env files.」とあります。

shared.envにs3を書いても.envのopendalに戻される実測

罠は、公式手順が cp .env.example .env から始まり、その雛形の159行目に STORAGE_TYPE=opendal がある点です。1.18.0のcomposeファイルで、.env を雛形のまま、shared.env に STORAGE_TYPE=s3 を書いて docker compose config(Docker Compose 5.6.0)で解決させた結果が次です。

書いた場所 api の STORAGE_TYPE plugin_daemon の PLUGIN_STORAGE_TYPE
shared.env だけ opendal local
.env の行を s3 に変更 s3 local
.env に PLUGIN_* も追記 s3 aws_s3

shared.env の S3_BUCKET_NAME はapiに届いているのに、STORAGE_TYPE だけが .env の opendal に上書きされ、保存先はローカルのままでした。.env を消すと「env file … .env not found」でcompose自体が止まるので、.env の該当行を s3 に書き換えます。

S3シークレットアクセスキーの.env経由での配布範囲

「全部 .env に書けば確実」という手順は動きますが、勧めません。同じ実測で S3_SECRET_KEY を .env に置くと、web(フロントエンド)・sandbox(コード実行)・agent_backend の環境変数にも入りました。公式リファレンスも PASSWORD_PEPPER の項で、同じ理由から .env に入れないよう書いています。

STORAGE_TYPE=s3 だけを .env に、鍵とバケット名は shared.env に分けます。AWS上ならIAMロールにして鍵そのものを置かないのが最善です。

S3バケットの事前作成とIAMポリシーの権限設計

Difyが呼ぶS3 APIから逆算した最小権限

apiの実装(api/extensions/storage/aws_s3_storage.py)が呼ぶのは head_bucket・put_object・get_object・head_object・delete_object・download_file・list_objects_v2(scan)です。プラグインデーモンが使う dify-cloud-kit v0.1.3 も HeadBucket・PutObject・GetObject・HeadObject・DeleteObject・ListObjectsV2 で、同じポリシーで足ります。

{
  "Version": "2012-10-17",
  "Statement": [
    {
      "Sid": "DifyObjects",
      "Effect": "Allow",
      "Action": ["s3:GetObject", "s3:PutObject", "s3:DeleteObject"],
      "Resource": "arn:aws:s3:::dify-storage-example/*"
    },
    {
      "Sid": "DifyBucket",
      "Effect": "Allow",
      "Action": ["s3:ListBucket"],
      "Resource": "arn:aws:s3:::dify-storage-example"
    }
  ]
}

2つ目のステートメントを省くと起動時の head_bucket が通りません。AWSのHeadBucketのAPIリファレンスは必要な権限を s3:ListBucket としており、これはオブジェクトARN(bucket/*)ではなくバケットARNに付けます。s3:DeleteObject を外すと、ドキュメントやアプリを削除してもS3にファイルが残り続けます。暗号化は2023年1月5日以降、指定が無ければSSE-S3が自動で掛かるので追加の権限は要りません(AWSの告知)。SSE-KMSに変えるなら、そのキーへの kms:GenerateDataKey と kms:Decrypt を足してください。

AmazonS3FullAccess はアカウント内の全バケットへの全操作を許すので、本番データと同じアカウントでは使わないでください。

バケットを事前に作る理由と公開設定

実装は起動時の head_bucket で404が返ると create_bucket を呼びますが、CreateBucketConfiguration を渡していません。CreateBucketのAPIリファレンスどおり、us-east-1以外のリージョンでは場所の指定が要るため、S3_REGION=ap-northeast-1 では自動作成が失敗します。CLIで先に作り、IAMからは s3:CreateBucket を外しておきます。

aws s3api create-bucket --bucket dify-storage-example \
  --region ap-northeast-1 \
  --create-bucket-configuration LocationConstraint=ap-northeast-1

バケットは公開しないでください。1.18.0のセルフホスト構成では、ブラウザへのファイル配信はDifyのAPIが FILES_URL 配下の署名付きURLで中継します。S3の署名付きURL(presigned URL)を発行するのは DEPLOYMENT_EDITION がCLOUDのときのアイコン表示だけで、ブラウザがバケットへ直接行く経路がありません。英語圏の解説にはバケットのCORS設定を必須とするものがありますが、既定構成のセルフホストでは不要です。2023年4月に全リージョンへの展開が完了してから作る一般用途バケットは、ブロックパブリックアクセスが既定で有効、ACLも無効です(AWSの告知)。この設定はそのまま残します。

S3認証方式別の環境変数設定

AWS外のサーバー向けアクセスキー認証設定

オンプレミスや他社クラウドで動かすときはIAMユーザーのアクセスキーを使います。docker/envs/core-services/shared.env が無ければ雛形からコピーして作ります。

# docker/.env(既存の行を書き換える)
STORAGE_TYPE=s3

# docker/envs/core-services/shared.env
S3_REGION=ap-northeast-1
S3_BUCKET_NAME=dify-storage-example
S3_ACCESS_KEY=AKIAIOSFODNN7EXAMPLE
S3_SECRET_KEY=wJalrXUtnFEMI/K7MDENG/bPxRfiCYEXAMPLE
S3_USE_AWS_MANAGED_IAM=false

AWSのS3なら S3_ENDPOINT は空でよく、boto3がリージョンから標準のエンドポイントを決めます。shared.env.example の S3_REGION=us-east-1 と S3_BUCKET_NAME=difyai は雛形の値で、コードの既定はどちらも未設定です。雛形をコピーして書き換え忘れると difyai というバケット名のまま接続を試み、その名前が他のアカウントに取られていれば403が握りつぶされて(後述)起動だけは成功します。反映は docker compose restart ではなく docker compose up -d で行ってください。restartはコンテナを作り直さないため、env_fileの変更が入りません。

EC2・ECS・EKS向けIAMロール認証設定

AWS上ではIAMロールを使います。S3_USE_AWS_MANAGED_IAM=true にすると、実装は boto3.Session() の標準の認証情報チェーンで、EC2のインスタンスプロファイル・ECSのタスクロール・EKSのIRSAやPod Identityから資格情報を得ます。

# docker/envs/core-services/shared.env
S3_USE_AWS_MANAGED_IAM=true
S3_REGION=ap-northeast-1
S3_BUCKET_NAME=dify-storage-example

この分岐でクライアントに渡るのは region_name だけで、S3_ENDPOINT と S3_ADDRESS_STYLE は使われません。プライベートサブネットからは、エンドポイントURLを変えずに済むゲートウェイ型のVPCエンドポイントでS3へ出ます。

EC2上のDocker Composeでは、コンテナからインスタンスメタデータ(IMDSv2)に届くかも確認します。AWSのEC2ユーザーガイドは、コンテナ環境ではホップ数の上限1だと応答を受け取れない場合があるとして、上限を2へ上げることを検討するよう案内しています。起動直後から認証エラーが続くなら、次で2に上げます。

aws ec2 modify-instance-metadata-options --instance-id i-0123456789abcdef0 \
  --http-endpoint enabled --http-tokens required --http-put-response-hop-limit 2

EC2での構築とBedrock接続はDifyをAWS EC2に構築しBedrockへIAMロール接続する手順、EKSではDifyをHelm chartでAWS EKSに構築する手順で扱っています。

MinIOなどS3互換ストレージの接続設定

S3互換ストレージも STORAGE_TYPE=s3 のまま S3_ENDPOINT を指定します。S3_ADDRESS_STYLE は auto・virtual・path を受け付け、boto3の addressing_style にそのまま渡ります。コンテナ名で参照する構成は bucket.minio の名前解決ができないので path にします。

# docker/envs/core-services/shared.env
S3_ENDPOINT=http://minio:9000
S3_REGION=us-east-1
S3_BUCKET_NAME=dify
S3_ACCESS_KEY=dify-app
S3_SECRET_KEY=change-me
S3_ADDRESS_STYLE=path
S3_USE_AWS_MANAGED_IAM=false

Difyのcomposeファイルにも minio サービスがありますが、これはMilvus用(container_name: milvus-minio、2023年3月版のイメージ)で、ファイル保存先として用意されたものではありません。MinIOのOSS版は2025年10月にビルド済みバイナリの提供をやめ、GitHubリポジトリは2026年4月25日にアーカイブ(読み取り専用)になりました。今から選ぶならMinIOのOSS版アーカイブ後の採用判断を先に確認してください。

既存ファイルとSECRET_KEYを移行する手順

運用中の環境を切り替えるときの手順です(新規構築なら不要)。移行が終わるまで既存ファイルとAPIキーが読めないので、メンテナンス時間帯に行い、実行中のワークフローや定期実行が終わったことを確かめます。docker compose stop nginx で入口を閉じても、サービス名なしの docker compose up -d はnginxも起動します。移行中は docker compose up -d api worker worker_beat plugin_daemon のようにサービスを指定して起動し、移行後にnginxを戻します。

S3切り替え前のSECRET_KEY固定

.env の SECRET_KEY= が空なら、いま使われている鍵はローカルの保存先にあります。その値を .env に書き写してから切り替えます。値を明示すれば、実装は保存先を見ずにその値を使います。

cd dify/docker
cat volumes/app/storage/.dify_secret_key
# 出力された値を docker/.env の SECRET_KEY= に書く

flask migrate-ossによる既存ファイルのS3移行

DifyのAPIには、ローカル(opendal)の保存先から現在の STORAGE_TYPE の保存先へコピーする flask migrate-oss があります。STORAGE_TYPE が opendal のままだと「Target STORAGE_TYPE must be a cloud OSS」で止まるので、S3の設定を反映してから実行します。

docker compose exec api flask migrate-oss --dry-run
docker compose exec api flask migrate-oss

既定でコピーする接頭辞は privkeys・upload_files・image_files・tools・website_files・keyword_files・ops_trace の7つです。--path を指定すると既定の7つは置き換わる(追加ではない)ため、ls volumes/app/storage でほかのディレクトリがあれば、まず既定どおり実行し、追加分だけを --path で別に流します。コピー後は upload_files のDBレコードの storage_type も書き換わります(--no-update-db で止められます)。読み書きのエラーがあれば --force なしではDB更新の前に確認が入りますが、接頭辞の走査エラーはこの確認の対象外なので、ログも見てください。

aws s3 sync volumes/app/storage s3://dify-storage-example でディレクトリごと送っても読み出しはできますが、DBの storage_type は古いままです。英語圏の記事にある flask upload-local-files-to-cloud-storage は1.18.0のソースに存在しません。

S3切り替え後の保存先・認証情報の検証

ナレッジにテスト用ファイルを1つ上げ、S3の upload_files/ 配下にオブジェクトが増えたかを確認します。Dify側の画面は保存先がローカルでも成功と表示されるので、判定はS3側で行います。あわせて、既存のモデルプロバイダーでチャットが返ること(秘密鍵の移行)と、ログインが切れていないこと(SECRET_KEY の固定)を確かめます。ローカルの旧データは、既存ファイルの読み出しと復元手順を確かめ、決めたロールバック期間が過ぎるまで残します。S3側の誤削除に備えるなら、バージョニングやS3 Object Lockによるイミュータブルバックアップを組み合わせます。

プラグインデーモンの保存先をS3へ移す設定

プラグインデーモンの保存先(インストール済みプラグインとパッケージのキャッシュ)は、STORAGE_TYPE とは別に PLUGIN_STORAGE_TYPE(既定 local)で決まります。単一ホストならローカルのままで困りませんが、ノードを入れ替える構成では再インストールが要ります。

注意点は書き場所です。composeの plugin_daemon は environment: で S3_USE_AWS: ${PLUGIN_S3_USE_AWS:-false} のように値を組み立てており、この ${…} はシェル環境変数、--env-file で指定したファイル、既定の .env などから展開されます。サービスの env_file は、この変数展開の入力にはなりません。shared.env.example には PLUGIN_S3_* の行がありますが、そこに書いても前掲の実測どおり local のままです。

# docker/.env に書く
PLUGIN_STORAGE_TYPE=aws_s3
# dify-plugin-exampleを事前作成し、このバケット用のIAM権限を追加する
# 前掲ポリシーのResourceを専用バケットのARNに置き換えたポリシーを別途付与する
PLUGIN_STORAGE_OSS_BUCKET=dify-plugin-example
PLUGIN_S3_USE_AWS=true
PLUGIN_S3_USE_AWS_MANAGED_IAM=true
PLUGIN_AWS_REGION=ap-northeast-1

PLUGIN_S3_USE_AWS=true は省けません。composeの既定は false で、その場合dify-cloud-kitはS3互換ストレージ用の分岐に入り、PLUGIN_S3_ENDPOINT を接続先URLとして使います。アクセスキー方式なら PLUGIN_S3_USE_AWS_MANAGED_IAM を外して PLUGIN_AWS_ACCESS_KEY・PLUGIN_AWS_SECRET_KEY を足します。プラグイン実行用の作業ディレクトリ(PLUGIN_WORKING_PATH=/app/storage/cwd)はローカルのままなので、volumes/plugin_daemon のマウントは外せません。インストール時のエラーはDifyのプラグインがインストールできない原因と対処法にまとめています。

保存に失敗するときの切り分け

「どこに書いたか」ではなく「コンテナに何が渡ったか」から見ます。

docker compose exec api env | grep -E "^(STORAGE_TYPE|S3_)"
docker compose exec plugin_daemon env | grep -E "^(PLUGIN_STORAGE_TYPE|S3_|AWS_REGION)"
  • STORAGE_TYPE=opendal が返る:.env の159行目付近がそのままです。shared.env 側の STORAGE_TYPE(雛形の499行目)も opendal のまま残りやすい行です。
  • 起動は成功するのにアップロードで失敗する:head_bucket が403を返しても実装は pass するため、IAMポリシーの不足やバケット名の誤りは最初の put_object まで表に出ません。
  • 起動時にCreateBucketのエラーで止まる:バケットが存在せず、リージョン指定の無い自動作成が失敗しています。バケット名の綴りを確かめ、前述のCLIで作成します。
  • モデルを呼ぶと Private key not found:privkeys が移っていません。flask migrate-oss --path privkeys を実行します。
  • 既存ドキュメントが「ファイルが見つかりません」:get_object の NoSuchKey が FileNotFoundError に変換されたものです。権限ではなく未移行を疑います。

AWS環境の設計から任せたい場合は、一創のインフラ構築(AWS・Google Cloud・Azure)でDifyを含む構成の支援を行っています。

よくある質問

shared.envにSTORAGE_TYPE=s3を書いたのに反映されないのはなぜですか?

最後に読まれる docker/.env に、雛形由来の STORAGE_TYPE=opendal が残っているためです。その行を s3 にして docker compose up -d で作り直します。

STORAGE_TYPEをopendalのままS3を使うことはできますか?

できます。OPENDAL_SCHEME=s3 にすると、Difyは OPENDAL_S3_ で始まる変数を集めてOpenDALに渡します(例:OPENDAL_S3_BUCKET・OPENDAL_S3_REGION)。この経路では S3_BUCKET_NAME などは読まれず、flask migrate-oss も移行先にopendalを受け付けません。AWSのS3なら STORAGE_TYPE=s3 の方が扱いやすいです。

S3へ切り替えると、それまでのファイルやAPIキーはどうなりますか?

自動では移りません。APIキーを復号する秘密鍵も同じ場所にあるため、flask migrate-oss で privkeys ごとコピーします。SECRET_KEY が空欄の環境は、先に値を固定しないとログイン状態に影響します。

Difyのデータをバックアップするには何を取ればよいですか?

ファイル保存先(S3またはvolumes/app/storage)に加えて、PostgreSQLのダンプ、ベクトルデータベースのボリューム、docker/.env と docker/envs/ の設定ファイルが必要です。S3はバケットのバージョニングで世代を残せますが、DBと時点をそろえないと参照切れが起きます。

MinIOなどのS3互換ストレージでも使えますか?

使えます。S3_ENDPOINT に互換ストレージのURLを、S3_ADDRESS_STYLE=path を指定します。ただし S3_USE_AWS_MANAGED_IAM=true の分岐ではエンドポイントが無視されるため、互換ストレージはアクセスキー方式で接続します。

関連記事

お気に入りに入れた記事の一覧

一創の公式 SNS

X では毎朝の新着記事と IT ニュースの速報を、YouTube・TikTok・Instagram・LinkedIn・Facebook では記事の解説動画を配信しています。

この記事は以下の記事からリンクされています

資料請求

今日のトレンド記事 直近 24 時間で、いつもより多く読まれている記事

  1. 2026.11.06 テックブログ ドコモ不正アクセス:最大29.5万件の流出項目と見舞金、My docomo停止の経過と偽連絡の見分け方
  2. 2026.11.03 テックブログ ネットマイル不正アクセスで最大549万人:対象確認と退会会員データを消す実装
  3. 2026.11.06 テックブログ あいおいニッセイ同和の不正アクセス:44.4万件の漏えい項目と窓口、止まった手続きの代わり
  4. 2026.11.06 テックブログ アイリスオーヤマ情報漏洩:アイリスプラザ770万人分の対象項目と本物の連絡の見分け方
  5. 2026.07.09 テックブログ Muse Imageとは?Metaの画像生成AIの使い方・API料金(1枚0.01ドル)と日本での提供状況【2026年10月】

RELATED POSTS 関連記事

目次