Dify

DifyをPodmanのrootlessで動かす手順とNotion連携の設定

Difyのセルフホストは Docker Compose が公式手順です。ところが RHEL 系のサーバーや、root 権限のあるデーモンを常駐させられない環境では、Podman を使わざるを得ない場面があります。Podman は Docker v1.40 互換のAPIを持つため、Dify が配布する compose ファイルはほぼそのまま流用できます。つまずくのは compose の中身ではなく、rootless という実行形態そのものが持つ制約です。この記事では Dify v1.16.1(2026年7月28日リリース)の実ファイルを確認しながら、ポート・SELinux・Notion連携の設定先という3つの詰まりどころを整理します。Docker が使える環境でAWS上に構築する場合は、DifyをAWS EC2に構築しBedrockへIAMロール接続する手順が前提を満たします。

まとめ

  • Podman の REST API には Docker v1.40 互換レイヤーがあるため、DOCKER_HOST に Podman のソケットを指すだけで Dify 公式の docker compose がそのまま通ります。compose ファイルの書き換えは不要です。
  • rootless では 1024 未満のポートに bind できません。Dify の docker/.env.exampleEXPOSE_NGINX_PORT=80 なので、ここが最初に止まる箇所です。
  • compose のバインドマウントは ./volumes/ 配下に47箇所ありますが、:z / :Z の再ラベル指定は1つもありません。SELinux が enforcing の RHEL 系では、そのままだとコンテナがマウント先を読めません。
  • Notion連携の NOTION_INTEGRATION_TYPE は root の docker/.env.example には無く、docker/envs/core-services/shared.env.example にあります。設定先を間違えると探しても見つかりません。

以下、ホスト側の前提設定から順に見ていきます。

Podmanで動かすためのホスト側の前提設定

Podman の rootless チュートリアルは「Rootless Podman requires the user running it to have a range of UIDs listed in the files /etc/subuid and /etc/subgid」と明記しています。書式は USERNAME:UID:RANGE で、値は各ユーザーで一意である必要があります。範囲が重なると、あるユーザーが別のユーザーの名前空間を壊せてしまうためです。

grep "^$(whoami):" /etc/subuid /etc/subgid

# 出力が無ければ割り当てる(範囲は既存ユーザーと重ならない値にする)
sudo usermod --add-subuids 100000-165535 --add-subgids 100000-165535 "$(whoami)"

ディストリビューションによっては既定で割り当て済みです。出力があればこの手順は不要です。

Dify 側の構成も確認しておきます。docker/docker-compose.yaml の冒頭には「WARNING: This file is auto-generated by generate_docker_compose / Do not modify this file directly」という警告が置かれており、生成元は .env.exampledocker-compose-template.yaml です。Podman で動かすからといってこのファイルを直接書き換える必要はありませんし、書き換えても次の更新で失われます。設定は環境変数側で吸収するのが前提の作りです。定義されているサービスは39個で、実際に起動するのはプロファイルで絞られた一部です。privileged: true が指定されているのは opengauss サービスだけなので、既定構成のまま rootless で動かす分には特権コンテナを要求されません。ここが Podman と相性の良い理由です。

Podmanソケットを使ったDifyの起動手順

podman.socketの有効化とDOCKER_HOSTの指定

Podman は常駐デーモンを持ちませんが、podman system service がオンデマンドで REST API を提供します。このAPIは「a compatibility layer offering support for the Docker v1.40 API」と Podman ネイティブの Libpod による2層構成で、前者があるおかげで docker-compose 系のツールがそのまま接続できます。rootless の既定ソケットは unix://$XDG_RUNTIME_DIR/podman/podman.sock、具体例としては unix:///run/user/1000/podman/podman.sock です。systemd のユーザーユニットは /usr/lib/systemd/user/podman.socket として配置されています。

systemctl --user enable --now podman.socket

export DOCKER_HOST=unix://$XDG_RUNTIME_DIR/podman/podman.sock
docker compose version

DOCKER_HOST はシェルを開き直すと消えます。運用するなら ~/.bashrc などに書いておきます。ログアウト後もコンテナを動かし続けたい場合は、対象ユーザーに sudo loginctl enable-linger "$(whoami)" を実行しておきます。これをしないと、セッション終了時にユーザー配下のサービスごと停止します。

Difyの取得から起動までのコマンド

Dify 側の手順は Docker のときと変わりません。公式サイトからインストーラを落とすような配布形態ではなく、リポジトリを取得して docker ディレクトリ内の compose を起動します。

git clone https://github.com/langgenius/dify.git
cd dify/docker
cp .env.example .env

docker compose up -d
docker compose ps

podman-compose を使う選択肢もありますが、Dify の compose は env_filepath 記法や required: false といった Compose Specification の比較的新しい構文を使っています。互換実装で解釈が食い違うと原因の切り分けが面倒になるため、DOCKER_HOST 経由で本家の docker compose を使うほうが確実です。Kubernetes 上で運用したい場合は compose ではなく Helm チャートを使う構成になるため、AWS EKSにDifyを導入するための事前準備と環境構築のポイントで前提条件を確認してください。

rootlessでポート80が塞がる原因と回避策

ここが Podman 固有の最初の壁です。Podman の troubleshooting ドキュメントは症状を「rootless podman fails with bind: permission denied」、原因を「Unprivileged users on a Linux system can not bind to ports below 1024 by default.」と説明しています。Dify の docker/.env.exampleEXPOSE_NGINX_PORT=80EXPOSE_NGINX_SSL_PORT=443 なので、コピーしたままの .env で起動すると nginx コンテナがこれに当たります。

取れる手は2つあり、性格がまったく違います。

回避策 変更対象 権限 影響範囲 向く場面
公開ポートを変更 docker/.env 不要 Difyのみ プロキシ前段あり
sysctlの下限を下げる /etc/sysctl.d/ sudo必須 ホスト全体 Dify単体で受ける

前段に ALB や nginx を置く構成なら、公開ポートの変更で十分です。Dify 側は非特権ポートで待ち受け、80番と443番は前段が引き受けます。ホストに手を入れずに済むので、共用サーバーではこちらを選びます。

# docker/.env
EXPOSE_NGINX_PORT=8080
EXPOSE_NGINX_SSL_PORT=8443

Dify 単体で80番を直接受ける必要がある場合は、非特権ユーザーが bind できるポートの下限そのものを下げます。/proc に直接書くと再起動で戻るため、設定ファイルに残します。

echo 'net.ipv4.ip_unprivileged_port_start=80' | sudo tee /etc/sysctl.d/99-podman-ports.conf
sudo sysctl --system

この設定はホスト上の全ユーザーに効きます。80番以上を誰でも bind できるようになるため、複数の利用者がログインするサーバーでは避けてください。下限を80にしておけば443番も同時に対象へ入ります。

SELinuxによるボリュームマウント失敗の回避

Podman が必須になる環境と、SELinux が enforcing で動いている環境は、実際にはほぼ重なります。RHEL や Fedora を選んだ時点で両方が付いてくるからです。そしてここに Dify 側の穴があります。

docker/docker-compose.yaml には ./volumes/ 配下へのバインドマウントが47箇所ありますが、SELinux の再ラベル指定である :z / :Z のサフィックスは1つも付いていません。- ./volumes/app/storage:/app/api/storage- ./volumes/db/data:/var/lib/postgresql/data といった記述がそのまま並んでいます。Docker では SELinux のラベル付けを既定で行わない運用が定着しているため問題になりませんが、Podman ではコンテナプロセスがホスト側ディレクトリのラベルを持たず、読み書きが拒否されます。ログには権限エラーだけが出るので、パーミッションの問題と誤診しやすい箇所です。

compose ファイルは自動生成物なので、:Z を書き足す修正は次の更新で消えます。ホスト側のディレクトリにコンテナ用のラベルを付けるほうが確実です。

# 起動して volumes/ が作られたあとに実行する
sudo chcon -Rt container_file_t ~/dify/docker/volumes

# 反映されたか確認する
ls -Z ~/dify/docker/volumes

SELinux を permissive に落とせば動きますが、Podman を選ぶ理由が隔離の強化である以上、それでは本末転倒です。ラベル付けで解決してください。

Notion連携の環境変数の設定先と反映確認

設定先はshared.envかdocker/.envか

セルフホストの Dify で Notion をナレッジのデータソースにする場合、公式ドキュメントは NOTION_INTEGRATION_TYPE=internalNOTION_INTERNAL_SECRET を設定するよう指示しています。ここで多くの人が止まります。docker ディレクトリで cp .env.example .env をした人が開く root の .env.example に、この項目が入っていないためです。

実体は docker/envs/core-services/shared.env.example にあり、NOTION_INTEGRATION_TYPE=publicNOTION_CLIENT_SECRETNOTION_CLIENT_IDNOTION_INTERNAL_SECRET が並んでいます。これは事故ではなく設計方針で、docker/README.md が「Keep the root .env.example limited to variables required to start the default Docker Compose deployment.」「place them in the appropriate envs/*.env.example file instead.」と規定しています。Notion は既定構成の起動に必須ではないので、分割ファイル側にあるわけです。

設定先は2つあり、どちらでも動きます。docker/envs/core-services/shared.env.exampleshared.env にコピーして書き換えるのが設計に沿った方法です。もう一方は root の docker/.env に直接追記する方法で、同じ README が「Docker Compose reads envs/*.env files when present, then reads .env last so values in .env take precedence.」と述べているとおり、.env の値が最後に読まれて優先されます。compose の各アンカーを見ても、envs/ 配下を required: false で並べた最後に - ./.env が置かれています。設定を1ファイルにまとめたいなら後者を選んでください。

# docker/.env に追記する場合
NOTION_INTEGRATION_TYPE=internal
NOTION_INTERNAL_SECRET=ntn_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

シークレットは Notion の Integrations ページで内部インテグレーションを作成し、Secrets セクションからコピーした値です。プレフィックスは新しく発行したものが ntn_、以前から使っているものは secret_ で、どちらも有効です。なお Notion 側の仕様として、インテグレーションを作成しただけでは対象ページにアクセスできません。連携したいページやデータベースを開き、接続先としてそのインテグレーションを追加する操作が別途必要です。

テンプレートに書かれた public を「未設定時の既定値」と誤解しないでください。コード上の定義(api/configs/extra/notion_config.py)では NOTION_INTEGRATION_TYPEdefaultNone で、説明も「Set to ‘internal’ for internal integrations, or None for public integrations.」となっています。未設定なら公開インテグレーション扱いで OAuth 経路に落ちるため、社内ワークスペースに繋ぐなら internal の明示が必要です。

連携が失敗したときの切り分け順序

環境変数を変えたら再起動します。公式ドキュメントも「新しい環境変数を反映させるため、Dify を再起動します」としています。

docker compose up -d --force-recreate api worker

# api コンテナに届いたか確認する
docker compose exec api env | grep NOTION

確認先を api に絞っているのには理由があります。NOTION_INTEGRATION_TYPENOTION_INTERNAL_SECRET を読むのは api コンテナのコンソール側の処理だけだからです。api/controllers/console/auth/data_source_oauth.py は、種別が internal ならシークレットを取り出し、空なら Internal secret is not set を返し、値があれば save_internal_access_token でデータベースへ保存します。

取り込みとインデックス処理自体は worker(Celery)側で走りますが、worker はこの環境変数を見ません。api/core/rag/extractor/notion_extractor.py はデータベースに保存された認証情報を読み出す作りで、そこで失敗したときのフォールバックも NOTION_INTERNAL_SECRET ではなく NOTION_INTEGRATION_TOKEN という別の変数です。したがって「ワークスペースの接続はできたのに worker に環境変数が届かず同期だけ失敗する」という筋書きは成立しません。設定漏れならバインドの時点で Internal secret is not set が出ます。

切り分けはこの順序になります。バインド時にエラーが出るなら api 側の環境変数、バインドは通るのに同期が失敗するならデータベース上の認証情報かNotion側のページ共有設定を疑ってください。worker のコンテナを作り直しても、前者は直りません。

Notion同期の制約と更新反映の運用

連携が通ったあとに認識のずれが起きやすいのが、同期の範囲と頻度です。公式ドキュメントは制約を「画像とファイルはインポートできません。テーブルのデータはテキストに変換されます」と明記しています。Notion に貼った図やPDFはナレッジに入らず、データベースやテーブルは構造を失ってテキストとして格納されます。仕様書の表を根拠に回答させたい場合、列と行の対応が崩れた状態で埋め込まれるため、期待した精度は出ません。

更新の反映も自動ではないと考えておくのが安全です。公式ドキュメントには「同期」設定に触れた記述と、ドキュメント一覧の同期ボタンをクリックして変更を反映する手順の両方がありますが、Celery の beat スケジュールに Notion 同期の定期タスクは定義されていません。実運用では手動同期を前提に、Notion のページを更新したら誰が同期を押すのかを決めておいてください。決めないまま運用すると、古い内容を答え続けるチャットボットになります。同期のたびに再度の埋め込み処理が走り、埋め込みモデルのトークンを消費する点も見込んでおく必要があります。

ナレッジの登録自体は管理画面から行います。データソースとして Notion を選び、ワークスペースをバインドし、取り込むページを選択してチャンク設定を決める流れです。取り込んだナレッジを実際の応答へつなぐ設計はDifyワークフローの基本機能と使い方の詳細、社内チャットボットとしての全体像は最近話題の「Dify」で社内チャットボットを作成しよう!概要と魅力を参照してください。Notion で扱えないファイル本体はオブジェクトストレージ側に置く構成が現実的で、その設定はDifyとの連携におけるAmazon S3の設定方法にまとめています。

Podmanを選ばないほうがよい場面

Podman を勧めない条件をはっきりさせておきます。Dify を Podman で動かす価値があるのは、rootless であることが要件になっている場合だけです。それ以外の理由で選ぶと、得るものより検証コストのほうが大きくなります。

Dify 本体は Docker Compose を前提に開発・テストされています。compose ファイルが generate_docker_compose で自動生成される運用である以上、バージョンが上がるたびに構文が更新される可能性があります。Podman 側の互換レイヤーが対応するのは Docker v1.40 API であり、compose の新しい構文がその上で意図通りに解釈されるかは、更新のたびに自分で確かめる作業になります。この確認を続けられないチームは Docker のままにすべきです。

プラグインデーモンのように追加のコンテナを動的に扱う仕組みも、ランタイムの差異が出やすい領域です。プラグインが入らないという症状に当たったとき、原因がプラグイン側なのかランタイム側なのかを切り分ける必要が生じます。Difyのプラグインがインストールできない原因と対処法【署名検証・容量・バージョン】で挙げた署名検証や容量の問題を先に潰してから、ランタイムを疑う順序が効率的です。

一方で、Podman が標準搭載で Docker のパッケージが用意されていない環境、あるいはセキュリティ要件で root デーモンの常駐が認められない環境では、Podman が唯一の現実解になります。この記事で挙げたポート・SELinux・環境変数の設定先という3点を押さえておけば、残りは Docker と同じ手順で進みます。

よくある質問

PodmanでもDify公式のdocker-compose.yamlはそのまま使えますか?

使えます。Podman の REST API は Docker v1.40 互換レイヤーを持つため、DOCKER_HOSTunix://$XDG_RUNTIME_DIR/podman/podman.sock を指定すれば docker compose がそのまま通ります。compose ファイル自体は自動生成物なので編集しないでください。

podman-composeとdocker composeのどちらを使うべきですか?

DOCKER_HOST 経由の docker compose を推奨します。Dify の compose は env_filepath 記法や required: false を使っており、互換実装では解釈差が問題の切り分けを難しくするためです。

NOTION_INTEGRATION_TYPEを.env.exampleで見つけられないのはなぜですか?

root の docker/.env.example ではなく docker/envs/core-services/shared.env.example に置かれているためです。docker/README.md が、既定構成の起動に不要な変数は envs/*.env.example 側に置くと規定しています。

Notion連携でInternal secret is not setと出たときはどこを見ますか?

api コンテナの環境変数です。docker compose exec api env | grep NOTIONNOTION_INTEGRATION_TYPE=internal とシークレットが入っているか確認してください。この判定は api 側で完結するため、worker を作り直しても解消しません。

sysctlでip_unprivileged_port_startを変更する影響はどこまで及びますか?

ホスト上の全ユーザーに及びます。80番以上を任意のユーザーが bind できる状態になるため、複数の利用者がログインするサーバーでは EXPOSE_NGINX_PORT を8080などに変更し、前段のプロキシで80番を受ける構成のほうが安全です。

関連記事

資料請求

RELATED POSTS 関連記事