AWS上のEC2 (RHEL9) で "Dify" と "Ollama" のスタンドアロン環境を作る

# はじめに
お疲れ様です。 @yuki_ink　です。

先日、以下の記事を投稿しました。
多くの方に "いいね" と "ストック" をしていただき、ローカル環境で生成AIを利用する、というユースケースの需要の高さを感じました。

https://qiita.com/yuki_ink/items/c3125f45fb725612910e

ただ、上記の記事の通りに実際にPCで構築をされた方ならお分かりかと思いますが、**処理がめちゃくちゃ遅いです。**
本格展開は普通に無理です。ユーザがストレスでPCを放り投げる未来が見えます。
まあ、ただのPCですしね。仕方がない。
```
じゃあ、AWS上にハイスペックのWindows Serverを立てて、同じ手順で Dify & Ollama を構築すればいいんじゃないの？
Bedrock使うのはちょっとハードル高いけど、EC2だけならすぐ使えるし！
```
ということで、やってみました！

**無理でした！**

まず、普通のインスタンスタイプではWSL 2 が使えませんでした。

>残念ながら通常のEC2は入れ子の仮想化をサポートしておらず、AWS上でWSL 2を利用したい場合はベアメタルインスタンスを使うしかありません。
WSL 2のためにベアメタルインスタンスを選ぶのは残念ながらコストに見合わないでしょう。
（出典）[EC2のWindows Server 2022でWSL 2を使うのは一筋縄ではいかないのでWSL 1で我慢してみた](https://dev.classmethod.jp/articles/how-to-use-wsl1-on-windows-server-2022/)

そこで WSL 1 を導入したものの、WSL 1 ではDockerが使えなかったというオチ。

https://qiita.com/riraosan/items/15d05ce33f028027a9d3

Windows Serverでの実現を早々に諦めて、Linuxでやることにしました。

# 環境と前提
今回は、AWS上に、以下の通りRHEL9サーバを構築しました。

| 項目 | 値 |
|-|-|
|AMI   |RHEL-9.4.0_HVM-20240605-x86_64-82-Hourly2-GP3   |
|インスタンスタイプ   |g5.2xlarge   |
|EBSボリューム   |50GiB  |

:::note
インスタンスタイプとEBSボリュームのサイズは、正確なサイジングを経てこの値になったというわけではなく、「ひとまずこれで作ってみるか」くらいのノリで決めたものなので、その点ご容赦ください。
（一応、GPU搭載のインスタンスタイプにはしたいという思いはあった。）
:::

また、以下2点を本記事の前提とさせていただきます。
- **適切なNW関連の設定がなされており、以下の通信が問題なくできること**
    - 作業端末からRHEL9サーバへの接続（SSH・HTTP・HTTPS）
        ※SSHはセッションマネージャで代替可
    - RHEL9サーバからインターネットへの接続
- **RHEL9サーバのroot権限が利用できること**

以下の手順は、ec2-userでRHEL9サーバにログインしている状態から始めます。


# Dockerの導入
こちらの記事を参考に、RHEL9サーバにDockerを導入します。

https://zenn.dev/hitoshiro/articles/4f00cfe6a2b213

### Dockerリポジトリの追加
>まずはDockerが提供するリポジトリをインストールする。

```
# ec2-userのホームディレクトリで作業します
$ cd

$ sudo dnf config-manager --add-repo https://download.docker.com/linux/centos/docker-ce.repo

Updating Subscription Management repositories.
Unable to read consumer identity

This system is not registered with an entitlement server. You can use "rhc" or "subscription-manager" to register.

Adding repo from: https://download.docker.com/linux/centos/docker-ce.repo
```
何やら色々メッセージが出てますが無視。
以下の手順でリポジトリが追加されていることが確認できればヨシ。

>リポジトリ一覧を確認する。
一番上、docker-ce-stableがDockerリポジトリだ。

```
$ sudo dnf repolist

Updating Subscription Management repositories.
Unable to read consumer identity

This system is not registered with an entitlement server. You can use "rhc" or "subscription-manager" to register.

repo id                                repo name
docker-ce-stable                       Docker CE Stable - x86_64
rhel-9-appstream-rhui-rpms             Red Hat Enterprise Linux 9 for x86_64 - AppStream from RHUI (RPMs)
rhel-9-baseos-rhui-rpms                Red Hat Enterprise Linux 9 for x86_64 - BaseOS from RHUI (RPMs)
rhui-client-config-server-9            Red Hat Enterprise Linux 9 Client Configuration
```

### Dockerエンジンをインストール
>下記コマンドで最新版のDockerエンジン、Dockerコマンドライン、Docker-compose（組み込みプラグイン）をインストールする。
```
$ sudo dnf -y install docker-ce docker-ce-cli containerd.io docker-compose-plugin

～中略～
Complete!
```

ここでDockerを起動。
ついでに自動起動も有効にしておきます。
```
$ sudo systemctl enable docker

$ sudo systemctl start docker
```

パーミッション関連の設定もやっておく。
```
$ sudo usermod -aG docker $USER

$ sudo chmod 666 /var/run/docker.sock
```
ちなみに `/var/run/docker.sock` とは。
>Dockerはデーモンプロセス（dockerd）と通信するクライアントとして動くわけですが、その際のソケットにはUNIXドメインソケット（/var/run/docker.sock）が利用されます。そのため、dockerdは特権的な動作が可能です。dockerdが使うUNIXドメインソケットに対して書き込み権限を有するということはルート権限があるのと同じことと言えます。
（出典）[マウントでdockerがどっかーん。](https://syu-m-5151.hatenablog.com/entry/2017/02/07/002845#varrundockersock%E3%81%A8%E3%81%AF)

これでDockerの導入が完了！
最後にDockerのバージョンを確認しておきます。
```
$ docker version

Client: Docker Engine - Community
 Version:           27.0.3
 API version:       1.46
 Go version:        go1.21.11
 Git commit:        7d4bcd8
 Built:             Sat Jun 29 00:04:07 2024
 OS/Arch:           linux/amd64
 Context:           default

Server: Docker Engine - Community
 Engine:
  Version:          27.0.3
  API version:      1.46 (minimum version 1.24)
  Go version:       go1.21.11
  Git commit:       662f78c
  Built:            Sat Jun 29 00:02:31 2024
  OS/Arch:          linux/amd64
  Experimental:     false
 containerd:
  Version:          1.7.18
  GitCommit:        ae71819c4f5e67bb4d5ae76a6b735f29cc25774e
 runc:
  Version:          1.7.18
  GitCommit:        v1.1.13-0-g58aa920
 docker-init:
  Version:          0.19.0
  GitCommit:        de40ad0
```

Docker Composeのバージョンも確認。
```
$ docker compose version

Docker Compose version v2.28.1
```

# Difyの導入

GitHubにdocker-composeが用意されているので、ダウンロードします。
```
$ sudo dnf install -y git

$ git clone https://github.com/langgenius/dify.git
```

`docker compose up` コマンドでDifyを起動します。
```
$ cd dify/docker

# -d オプションをつけて、バックグラウンドで起動
$ docker compose up -d
```

起動が完了したら、作業端末から `http://{RHEL9サーバのIPアドレス}/install` にアクセスします。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/19aa717c-81d9-19a9-5656-0b0af6f2a2c5.png)

開いた画面でサインアップし、続いてサインインします。
スタジオ画面に遷移すればOK！
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/81eba7f8-cbb6-8e5c-5c14-5b82a9d9352f.png)

日本語表示になっていなければ、ここでかっこつけずに言語設定を変えておきましょう。
画面右上のユーザー名をクリックし、表示されるメニューから `Settings` を選択。
左のメニューから `Language` を選択します。
プルダウンから日本語を選べば設定完了。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/ca7888c9-be16-3343-e36d-ed7e8cd734c9.png)

言語設定の下にタイムゾーン設定のプルダウンもあるので、ついでに変更しておきます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/5b7e0116-87a1-0e38-e494-3ec00dd524ad.png)


# Ollamaの導入

[公式](https://ollama.com/download/linux)から案内されている通り、RHEL9サーバで `curl -fsSL https://ollama.com/install.sh | sh` コマンドを実行します。
```
# ec2-userのホームディレクトリで作業します
$ cd

$ curl -fsSL https://ollama.com/install.sh | sh

～中略～
Complete!
>>> NVIDIA GPU ready.
>>> The Ollama API is now available at 127.0.0.1:11434.
>>> Install complete. Run "ollama" from the command line.
```
:::note
参考までに、`ollama.com/install.sh` の中身（2024/7/6時点）を載せておきます。
Ollama本体のインストールに加え、OSやハードウェアアーキテクチャの情報を取得し、適切なパッケージ・ドライバのインストールなどを行っていることがわかります。
自力で頑張ってインストールを行うより、大人しく公式のスクリプトを使っておいた方がよさそうです。

<details><summary>（参考）ollama.com/install.sh の中身</summary>



```ollama.com/install.sh
#!/bin/sh
# This script installs Ollama on Linux.
# It detects the current operating system architecture and installs the appropriate version of Ollama.

set -eu

status() { echo ">>> $*" >&2; }
error() { echo "ERROR $*"; exit 1; }
warning() { echo "WARNING: $*"; }

TEMP_DIR=$(mktemp -d)
cleanup() { rm -rf $TEMP_DIR; }
trap cleanup EXIT

available() { command -v $1 >/dev/null; }
require() {
    local MISSING=''
    for TOOL in $*; do
        if ! available $TOOL; then
            MISSING="$MISSING $TOOL"
        fi
    done

    echo $MISSING
}

[ "$(uname -s)" = "Linux" ] || error 'This script is intended to run on Linux only.'

ARCH=$(uname -m)
case "$ARCH" in
    x86_64) ARCH="amd64" ;;
    aarch64|arm64) ARCH="arm64" ;;
    *) error "Unsupported architecture: $ARCH" ;;
esac

IS_WSL2=false

KERN=$(uname -r)
case "$KERN" in
    *icrosoft*WSL2 | *icrosoft*wsl2) IS_WSL2=true;;
    *icrosoft) error "Microsoft WSL1 is not currently supported. Please upgrade to WSL2 with 'wsl --set-version <distro> 2'" ;;
    *) ;;
esac

VER_PARAM="${OLLAMA_VERSION:+?version=$OLLAMA_VERSION}"

SUDO=
if [ "$(id -u)" -ne 0 ]; then
    # Running as root, no need for sudo
    if ! available sudo; then
        error "This script requires superuser permissions. Please re-run as root."
    fi

    SUDO="sudo"
fi

NEEDS=$(require curl awk grep sed tee xargs)
if [ -n "$NEEDS" ]; then
    status "ERROR: The following tools are required but missing:"
    for NEED in $NEEDS; do
        echo "  - $NEED"
    done
    exit 1
fi

status "Downloading ollama..."
curl --fail --show-error --location --progress-bar -o $TEMP_DIR/ollama "https://ollama.com/download/ollama-linux-${ARCH}${VER_PARAM}"

for BINDIR in /usr/local/bin /usr/bin /bin; do
    echo $PATH | grep -q $BINDIR && break || continue
done

status "Installing ollama to $BINDIR..."
$SUDO install -o0 -g0 -m755 -d $BINDIR
$SUDO install -o0 -g0 -m755 $TEMP_DIR/ollama $BINDIR/ollama

install_success() {
    status 'The Ollama API is now available at 127.0.0.1:11434.'
    status 'Install complete. Run "ollama" from the command line.'
}
trap install_success EXIT

# Everything from this point onwards is optional.

configure_systemd() {
    if ! id ollama >/dev/null 2>&1; then
        status "Creating ollama user..."
        $SUDO useradd -r -s /bin/false -U -m -d /usr/share/ollama ollama
    fi
    if getent group render >/dev/null 2>&1; then
        status "Adding ollama user to render group..."
        $SUDO usermod -a -G render ollama
    fi
    if getent group video >/dev/null 2>&1; then
        status "Adding ollama user to video group..."
        $SUDO usermod -a -G video ollama
    fi

    status "Adding current user to ollama group..."
    $SUDO usermod -a -G ollama $(whoami)

    status "Creating ollama systemd service..."
    cat <<EOF | $SUDO tee /etc/systemd/system/ollama.service >/dev/null
[Unit]
Description=Ollama Service
After=network-online.target

[Service]
ExecStart=$BINDIR/ollama serve
User=ollama
Group=ollama
Restart=always
RestartSec=3
Environment="PATH=$PATH"

[Install]
WantedBy=default.target
EOF
    SYSTEMCTL_RUNNING="$(systemctl is-system-running || true)"
    case $SYSTEMCTL_RUNNING in
        running|degraded)
            status "Enabling and starting ollama service..."
            $SUDO systemctl daemon-reload
            $SUDO systemctl enable ollama

            start_service() { $SUDO systemctl restart ollama; }
            trap start_service EXIT
            ;;
    esac
}

if available systemctl; then
    configure_systemd
fi

# WSL2 only supports GPUs via nvidia passthrough
# so check for nvidia-smi to determine if GPU is available
if [ "$IS_WSL2" = true ]; then
    if available nvidia-smi && [ -n "$(nvidia-smi | grep -o "CUDA Version: [0-9]*\.[0-9]*")" ]; then
        status "Nvidia GPU detected."
    fi
    install_success
    exit 0
fi

# Install GPU dependencies on Linux
if ! available lspci && ! available lshw; then
    warning "Unable to detect NVIDIA/AMD GPU. Install lspci or lshw to automatically detect and install GPU dependencies."
    exit 0
fi

check_gpu() {
    # Look for devices based on vendor ID for NVIDIA and AMD
    case $1 in
        lspci)
            case $2 in
                nvidia) available lspci && lspci -d '10de:' | grep -q 'NVIDIA' || return 1 ;;
                amdgpu) available lspci && lspci -d '1002:' | grep -q 'AMD' || return 1 ;;
            esac ;;
        lshw)
            case $2 in
                nvidia) available lshw && $SUDO lshw -c display -numeric -disable network | grep -q 'vendor: .* \[10DE\]' || return 1 ;;
                amdgpu) available lshw && $SUDO lshw -c display -numeric -disable network | grep -q 'vendor: .* \[1002\]' || return 1 ;;
            esac ;;
        nvidia-smi) available nvidia-smi || return 1 ;;
    esac
}

if check_gpu nvidia-smi; then
    status "NVIDIA GPU installed."
    exit 0
fi

if ! check_gpu lspci nvidia && ! check_gpu lshw nvidia && ! check_gpu lspci amdgpu && ! check_gpu lshw amdgpu; then
    install_success
    warning "No NVIDIA/AMD GPU detected. Ollama will run in CPU-only mode."
    exit 0
fi

if check_gpu lspci amdgpu || check_gpu lshw amdgpu; then
    # Look for pre-existing ROCm v6 before downloading the dependencies
    for search in "${HIP_PATH:-''}" "${ROCM_PATH:-''}" "/opt/rocm" "/usr/lib64"; do
        if [ -n "${search}" ] && [ -e "${search}/libhipblas.so.2" -o -e "${search}/lib/libhipblas.so.2" ]; then
            status "Compatible AMD GPU ROCm library detected at ${search}"
            install_success
            exit 0
        fi
    done

    status "Downloading AMD GPU dependencies..."
    $SUDO rm -rf /usr/share/ollama/lib
    $SUDO chmod o+x /usr/share/ollama
    $SUDO install -o ollama -g ollama -m 755 -d /usr/share/ollama/lib/rocm
    curl --fail --show-error --location --progress-bar "https://ollama.com/download/ollama-linux-amd64-rocm.tgz${VER_PARAM}" \
        | $SUDO tar zx --owner ollama --group ollama -C /usr/share/ollama/lib/rocm .
    install_success
    status "AMD GPU ready."
    exit 0
fi

# ref: https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html#rhel-7-centos-7
# ref: https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html#rhel-8-rocky-8
# ref: https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html#rhel-9-rocky-9
# ref: https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html#fedora
install_cuda_driver_yum() {
    status 'Installing NVIDIA repository...'
    case $PACKAGE_MANAGER in
        yum)
            $SUDO $PACKAGE_MANAGER -y install yum-utils
            $SUDO $PACKAGE_MANAGER-config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/$1$2/$(uname -m)/cuda-$1$2.repo
            ;;
        dnf)
            $SUDO $PACKAGE_MANAGER config-manager --add-repo https://developer.download.nvidia.com/compute/cuda/repos/$1$2/$(uname -m)/cuda-$1$2.repo
            ;;
    esac

    case $1 in
        rhel)
            status 'Installing EPEL repository...'
            # EPEL is required for third-party dependencies such as dkms and libvdpau
            $SUDO $PACKAGE_MANAGER -y install https://dl.fedoraproject.org/pub/epel/epel-release-latest-$2.noarch.rpm || true
            ;;
    esac

    status 'Installing CUDA driver...'

    if [ "$1" = 'centos' ] || [ "$1$2" = 'rhel7' ]; then
        $SUDO $PACKAGE_MANAGER -y install nvidia-driver-latest-dkms
    fi

    $SUDO $PACKAGE_MANAGER -y install cuda-drivers
}

# ref: https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html#ubuntu
# ref: https://docs.nvidia.com/cuda/cuda-installation-guide-linux/index.html#debian
install_cuda_driver_apt() {
    status 'Installing NVIDIA repository...'
    curl -fsSL -o $TEMP_DIR/cuda-keyring.deb https://developer.download.nvidia.com/compute/cuda/repos/$1$2/$(uname -m)/cuda-keyring_1.1-1_all.deb

    case $1 in
        debian)
            status 'Enabling contrib sources...'
            $SUDO sed 's/main/contrib/' < /etc/apt/sources.list | $SUDO tee /etc/apt/sources.list.d/contrib.list > /dev/null
            if [ -f "/etc/apt/sources.list.d/debian.sources" ]; then
                $SUDO sed 's/main/contrib/' < /etc/apt/sources.list.d/debian.sources | $SUDO tee /etc/apt/sources.list.d/contrib.sources > /dev/null
            fi
            ;;
    esac

    status 'Installing CUDA driver...'
    $SUDO dpkg -i $TEMP_DIR/cuda-keyring.deb
    $SUDO apt-get update

    [ -n "$SUDO" ] && SUDO_E="$SUDO -E" || SUDO_E=
    DEBIAN_FRONTEND=noninteractive $SUDO_E apt-get -y install cuda-drivers -q
}

if [ ! -f "/etc/os-release" ]; then
    error "Unknown distribution. Skipping CUDA installation."
fi

. /etc/os-release

OS_NAME=$ID
OS_VERSION=$VERSION_ID

PACKAGE_MANAGER=
for PACKAGE_MANAGER in dnf yum apt-get; do
    if available $PACKAGE_MANAGER; then
        break
    fi
done

if [ -z "$PACKAGE_MANAGER" ]; then
    error "Unknown package manager. Skipping CUDA installation."
fi

if ! check_gpu nvidia-smi || [ -z "$(nvidia-smi | grep -o "CUDA Version: [0-9]*\.[0-9]*")" ]; then
    case $OS_NAME in
        centos|rhel) install_cuda_driver_yum 'rhel' $(echo $OS_VERSION | cut -d '.' -f 1) ;;
        rocky) install_cuda_driver_yum 'rhel' $(echo $OS_VERSION | cut -c1) ;;
        fedora) [ $OS_VERSION -lt '39' ] && install_cuda_driver_yum $OS_NAME $OS_VERSION || install_cuda_driver_yum $OS_NAME '39';;
        amzn) install_cuda_driver_yum 'fedora' '37' ;;
        debian) install_cuda_driver_apt $OS_NAME $OS_VERSION ;;
        ubuntu) install_cuda_driver_apt $OS_NAME $(echo $OS_VERSION | sed 's/\.//') ;;
        *) exit ;;
    esac
fi

if ! lsmod | grep -q nvidia || ! lsmod | grep -q nvidia_uvm; then
    KERNEL_RELEASE="$(uname -r)"
    case $OS_NAME in
        rocky) $SUDO $PACKAGE_MANAGER -y install kernel-devel kernel-headers ;;
        centos|rhel|amzn) $SUDO $PACKAGE_MANAGER -y install kernel-devel-$KERNEL_RELEASE kernel-headers-$KERNEL_RELEASE ;;
        fedora) $SUDO $PACKAGE_MANAGER -y install kernel-devel-$KERNEL_RELEASE ;;
        debian|ubuntu) $SUDO apt-get -y install linux-headers-$KERNEL_RELEASE ;;
        *) exit ;;
    esac

    NVIDIA_CUDA_VERSION=$($SUDO dkms status | awk -F: '/added/ { print $1 }')
    if [ -n "$NVIDIA_CUDA_VERSION" ]; then
        $SUDO dkms install $NVIDIA_CUDA_VERSION
    fi

    if lsmod | grep -q nouveau; then
        status 'Reboot to complete NVIDIA CUDA driver install.'
        exit 0
    fi

    $SUDO modprobe nvidia
    $SUDO modprobe nvidia_uvm
fi

# make sure the NVIDIA modules are loaded on boot with nvidia-persistenced
if command -v nvidia-persistenced > /dev/null 2>&1; then
    $SUDO touch /etc/modules-load.d/nvidia.conf
    MODULES="nvidia nvidia-uvm"
    for MODULE in $MODULES; do
        if ! grep -qxF "$MODULE" /etc/modules-load.d/nvidia.conf; then
            echo "$MODULE" | sudo tee -a /etc/modules-load.d/nvidia.conf > /dev/null
        fi
    done
fi

status "NVIDIA GPU ready."
install_success
```
</details>
:::

# Ollama で Llama-3-ELYZA-JP-8B を動かしてみる

https://qiita.com/s3kzk/items/3cebb8d306fb46cabe9f

**僕も Ollama で Llama-3-ELYZA-JP-8B 動かしたい～！！(ﾟ∀ﾟ)**
ということでやってみます。

### 1. モデルファイルのダウンロード
>Hugging Face から、「Llama-3-ELYZA-JP-8B-q4_k_m.gguf」をダウンロードします。これは量子化されたモデルファイルで、サイズが小さくなっています。
サイズは約5GBですが、不安な人はここでついでにストレージの整理をしておきましょう（しなくていいです）

今回はRHELでの作業なので、curlで取ってきます。
```
# ec2-userのホームディレクトリで作業します
$ cd

$ curl -OL https://huggingface.co/elyza/Llama-3-ELYZA-JP-8B-GGUF/resolve/main/Llama-3-ELYZA-JP-8B-q4_k_m.gguf
```
:::note
GUIで取ってくる場合は[こちら](https://huggingface.co/elyza/Llama-3-ELYZA-JP-8B-GGUF/tree/main)から。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/511fd3e9-1000-f816-dad0-e6b8fd4892e1.png)
:::


### 2. Modelfileの作成
> 今回試したいモデルは、Ollama では標準サポートされていないため、モデルに関連する必要な情報を記載したファイルを作成し、そのファイルからモデルを作ります。

viでファイルを作っていきます。
```
$ vi Modelfile

## 以下を追記して保存する

FROM ./Llama-3-ELYZA-JP-8B-q4_k_m.gguf
TEMPLATE """{{ if .System }}
system


{{ .System }}
{{ end }}{{ if .Prompt }}
user


{{ .Prompt }}
{{ end }}
assistant


{{ .Response }}
"""
PARAMETER stop "
"
PARAMETER stop "
"
PARAMETER stop "
"
PARAMETER stop "<|reserved_special_token"
```

### 3. Ollamaモデルの作成
>次のコマンドを実行して、Modelfileからollamaのモデルを作成します：

```
$ ollama create elyza:jp8b -f Modelfile

～中略～
writing manifest
success
```

### 4. モデルの実行
>作成したモデルを実行するには、以下のコマンドを使用します：
```
$ ollama run elyza:jp8b
```

チャットセッションが開始されたので、「Amazon S3が主役の物語を日本語で作ってください」とお願いしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/4d2d88f7-20ec-607c-4034-e6a4fd365856.png)
草。

コンソールに `/bye` と打ち込んで、チャットセッションを抜けます。



# DifyとOllamaの連係

### Ollama側の設定
まず、systemdの定義ファイルを修正し、Ollamaの環境変数を設定します。
```
$ sudo systemctl edit ollama.service

## 以下を追記して保存する

[Service]
Environment=OLLAMA_HOST=0.0.0.0
Environment=OLLAMA_ORIGINS=*
```

:::note
追記する場所は以下を参考にしてください。

```diff
### Editing /etc/systemd/system/ollama.service.d/override.conf
### Anything between here and the comment below will become the new contents of the file

+[Service]
+Environment=OLLAMA_HOST=0.0.0.0
+Environment=OLLAMA_ORIGINS=*

### Lines below this comment will be discarded

### /etc/systemd/system/ollama.service
# [Unit]
# Description=Ollama Service
# After=network-online.target
～後略～
```
:::

:::note warn
Ollamaへの接続元を制御する環境変数 `OLLAMA_ORIGINS` に `*` を入れており、Ollama側で接続元の制御を行わない（全てのリクエストを受け付ける）設定にしています。
Security Group や Network ACL を利用し、NWレベルでアクセス制御を実施してください。
:::

設定を反映するために、サービスを再起動します。
その後curlコマンドを打って、`Ollama is running` と返ってくればOK！
```
$ sudo systemctl restart ollama

$ curl http://{RHEL9サーバのプライベートIPアドレス}:11434
Ollama is running
```

### Dify側の設定（モデルの追加）
画面右上のユーザー名をクリックし、表示されるメニューから `設定` を選択。
左のメニューから `モデルプロバイダー` を選択します。

Ollamaがいました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/8814af0b-84c8-3100-f764-ed27e36a9ed5.png)

セットアップ画面を開き、パラメータを入れていきます。
入力が終わったら `保存` ボタンをクリック。
```
> Model Name
elyza:jp8b

> Base URL
http://{RHEL9サーバのプライベートIPアドレス}:11434

※他はひとまずデフォルト値
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/c7031b0c-2d8d-2f2b-749f-18f28927f1a9.png)

これで設定は完了。
チャットボットを作ってテストしてみます。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/670c9a56-749e-2365-b909-c70a19d0cb54.png)

右上に `elyza:jp8b` が表示されていることを確認し、プレビュー画面にプロンプトを入力。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/723d2fe9-e647-48c7-ba2b-79715c02c4d6.png)
草。

画面右上の `公開する` ボタンをクリックし、続いて　`アプリを実行` をクリックすると、そのままチャットボットアプリを利用できるようになります。

:::note
発行されたURL `http://{RHEL9サーバのプライベートIPアドレス}/chat/{ランダム文字列}` を展開すれば、アプリケーションを共有できるようになります！
私が試した限り、チャットの履歴は接続元の端末ごとに管理され、他端末でのやりとりを閲覧することはできません。
- 同じWi-Fiに接続し、グローバルIPアドレスが同一の端末AとBを用意したとき、端末Aと端末Bのチャットは互いに共有されない。
- 一度接続を落とし、再度接続しても、過去のチャットを参照できる
- RHEL9サーバをリブートしても、チャットの履歴は保持される
:::

# Tips

いくつかのTipsをまとめます。

## `host.docker.internal` でOllamaに接続する

Difyにモデルを追加する際、今回は `Base URL` に `http://{RHEL9サーバのプライベートIPアドレス}:11434` を入れましたが、もう少しスマートに (？) やろうとすると、`http://host.docker.internal:11434` を入れるやり方もあります。

それをやろうとすると、`docker-compose.yaml` の修正が必要になります。

>以下の設定で `host.docker.internal` がホストのアドレスに解決される。よってコンテナ内のプロセスからこのドメイン名に接続するとコンテナ外のホストのプロセスに接続できる（ホストは `0.0.0.0` 等のアドレスで待ち受ける必要あり）。
>```
>services:
>  SERVICE:
>    image: IMAGE
>    extra_hosts:
>      - host.docker.internal:host-gateway
>```
>`extra_hosts` でコンテナ内の `/etc/hosts` にエントリを追加できる。ここで `host-gateway` を名前解決先に指定するとホストのアドレスにしてくれる模様。
>（出典）[Dockerでコンテナ内のプロセスからホストのプロセスに接続する方法](https://qiita.com/silane1001/items/973f32e05defe254408c)


 `host.docker.internal` を修正したら、こんな感じ。
 
<details><summary>修正後の docker-compose.yaml</summary>

```diff:docker-compose.yaml
x-shared-env: &shared-api-worker-env
  LOG_LEVEL: ${LOG_LEVEL:-INFO}
  DEBUG: ${DEBUG:-false}
  FLASK_DEBUG: ${FLASK_DEBUG:-false}
  SECRET_KEY: ${SECRET_KEY:-sk-9f73s3ljTXVcMT3Blb3ljTqtsKiGHXVcMT3BlbkFJLK7U}
  INIT_PASSWORD: ${INIT_PASSWORD:-}
  CONSOLE_WEB_URL: ${CONSOLE_WEB_URL:-}
  CONSOLE_API_URL: ${CONSOLE_API_URL:-}
  SERVICE_API_URL: ${SERVICE_API_URL:-}
  APP_WEB_URL: ${APP_WEB_URL:-}
  CHECK_UPDATE_URL: ${CHECK_UPDATE_URL:-https://updates.dify.ai}
  OPENAI_API_BASE: ${OPENAI_API_BASE:-https://api.openai.com/v1}
  FILES_URL: ${FILES_URL:-}
  FILES_ACCESS_TIMEOUT: ${FILES_ACCESS_TIMEOUT:-300}
  MIGRATION_ENABLED: ${MIGRATION_ENABLED:-true}
  DEPLOY_ENV: ${DEPLOY_ENV:-PRODUCTION}
  DIFY_BIND_ADDRESS: ${DIFY_BIND_ADDRESS:-0.0.0.0}
  DIFY_PORT: ${DIFY_PORT:-5001}
  SERVER_WORKER_AMOUNT: ${SERVER_WORKER_AMOUNT:-}
  SERVER_WORKER_CLASS: ${SERVER_WORKER_CLASS:-}
  CELERY_WORKER_CLASS: ${CELERY_WORKER_CLASS:-}
  GUNICORN_TIMEOUT: ${GUNICORN_TIMEOUT:-360}
  CELERY_WORKER_AMOUNT: ${CELERY_WORKER_AMOUNT:-}
  DB_USERNAME: ${DB_USERNAME:-postgres}
  DB_PASSWORD: ${DB_PASSWORD:-difyai123456}
  DB_HOST: ${DB_HOST:-db}
  DB_PORT: ${DB_PORT:-5432}
  DB_DATABASE: ${DB_DATABASE:-dify}
  SQLALCHEMY_POOL_SIZE: ${SQLALCHEMY_POOL_SIZE:-30}
  SQLALCHEMY_POOL_RECYCLE: ${SQLALCHEMY_POOL_RECYCLE:-3600}
  SQLALCHEMY_ECHO: ${SQLALCHEMY_ECHO:-false}
  REDIS_HOST: ${REDIS_HOST:-redis}
  REDIS_PORT: ${REDIS_PORT:-6379}
  REDIS_USERNAME: ${REDIS_USERNAME:-}
  REDIS_PASSWORD: ${REDIS_PASSWORD:-difyai123456}
  REDIS_USE_SSL: ${REDIS_USE_SSL:-false}
  REDIS_DB: 0
  CELERY_BROKER_URL: ${CELERY_BROKER_URL:-redis://:difyai123456@redis:6379/1}
  BROKER_USE_SSL: ${BROKER_USE_SSL:-false}
  WEB_API_CORS_ALLOW_ORIGINS: ${WEB_API_CORS_ALLOW_ORIGINS:-*}
  CONSOLE_CORS_ALLOW_ORIGINS: ${CONSOLE_CORS_ALLOW_ORIGINS:-*}
  STORAGE_TYPE: ${STORAGE_TYPE:-local}
  STORAGE_LOCAL_PATH: storage
  S3_USE_AWS_MANAGED_IAM: ${S3_USE_AWS_MANAGED_IAM:-false}
  S3_ENDPOINT: ${S3_ENDPOINT:-}
  S3_BUCKET_NAME: ${S3_BUCKET_NAME:-}
  S3_ACCESS_KEY: ${S3_ACCESS_KEY:-}
  S3_SECRET_KEY: ${S3_SECRET_KEY:-}
  S3_REGION: ${S3_REGION:-us-east-1}
  AZURE_BLOB_ACCOUNT_NAME: ${AZURE_BLOB_ACCOUNT_NAME:-}
  AZURE_BLOB_ACCOUNT_KEY: ${AZURE_BLOB_ACCOUNT_KEY:-}
  AZURE_BLOB_CONTAINER_NAME: ${AZURE_BLOB_CONTAINER_NAME:-}
  AZURE_BLOB_ACCOUNT_URL: ${AZURE_BLOB_ACCOUNT_URL:-}
  GOOGLE_STORAGE_BUCKET_NAME: ${GOOGLE_STORAGE_BUCKET_NAME:-}
  GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64: ${GOOGLE_STORAGE_SERVICE_ACCOUNT_JSON_BASE64:-}
  ALIYUN_OSS_BUCKET_NAME: ${ALIYUN_OSS_BUCKET_NAME:-}
  ALIYUN_OSS_ACCESS_KEY: ${ALIYUN_OSS_ACCESS_KEY:-}
  ALIYUN_OSS_SECRET_KEY: ${ALIYUN_OSS_SECRET_KEY:-}
  ALIYUN_OSS_ENDPOINT: ${ALIYUN_OSS_ENDPOINT:-}
  ALIYUN_OSS_REGION: ${ALIYUN_OSS_REGION:-}
  ALIYUN_OSS_AUTH_VERSION: ${ALIYUN_OSS_AUTH_VERSION:-v4}
  TENCENT_COS_BUCKET_NAME: ${TENCENT_COS_BUCKET_NAME:-}
  TENCENT_COS_SECRET_KEY: ${TENCENT_COS_SECRET_KEY:-}
  TENCENT_COS_SECRET_ID: ${TENCENT_COS_SECRET_ID:-}
  TENCENT_COS_REGION: ${TENCENT_COS_REGION:-}
  TENCENT_COS_SCHEME: ${TENCENT_COS_SCHEME:-}
  OCI_ENDPOINT: ${OCI_ENDPOINT:-}
  OCI_BUCKET_NAME: ${OCI_BUCKET_NAME:-}
  OCI_ACCESS_KEY: ${OCI_ACCESS_KEY:-}
  OCI_SECRET_KEY: ${OCI_SECRET_KEY:-}
  OCI_REGION: ${OCI_REGION:-}
  VECTOR_STORE: ${VECTOR_STORE:-weaviate}
  WEAVIATE_ENDPOINT: ${WEAVIATE_ENDPOINT:-http://weaviate:8080}
  WEAVIATE_API_KEY: ${WEAVIATE_API_KEY:-WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih}
  QDRANT_URL: ${QDRANT_URL:-http://qdrant:6333}
  QDRANT_API_KEY: ${QDRANT_API_KEY:-difyai123456}
  QDRANT_CLIENT_TIMEOUT: ${QDRANT_CLIENT_TIMEOUT:-20}
  QDRANT_GRPC_ENABLED: ${QDRANT_GRPC_ENABLED:-false}
  QDRANT_GRPC_PORT: ${QDRANT_GRPC_PORT:-6334}
  MILVUS_HOST: ${MILVUS_HOST:-127.0.0.1}
  MILVUS_PORT: ${MILVUS_PORT:-19530}
  MILVUS_USER: ${MILVUS_USER:-root}
  MILVUS_PASSWORD: ${MILVUS_PASSWORD:-Milvus}
  MILVUS_SECURE: ${MILVUS_SECURE:-false}
  RELYT_HOST: ${RELYT_HOST:-db}
  RELYT_PORT: ${RELYT_PORT:-5432}
  RELYT_USER: ${RELYT_USER:-postgres}
  RELYT_PASSWORD: ${RELYT_PASSWORD:-difyai123456}
  RELYT_DATABASE: ${RELYT_DATABASE:-postgres}
  PGVECTOR_HOST: ${PGVECTOR_HOST:-pgvector}
  PGVECTOR_PORT: ${PGVECTOR_PORT:-5432}
  PGVECTOR_USER: ${PGVECTOR_USER:-postgres}
  PGVECTOR_PASSWORD: ${PGVECTOR_PASSWORD:-difyai123456}
  PGVECTOR_DATABASE: ${PGVECTOR_DATABASE:-dify}
  TIDB_VECTOR_HOST: ${TIDB_VECTOR_HOST:-tidb}
  TIDB_VECTOR_PORT: ${TIDB_VECTOR_PORT:-4000}
  TIDB_VECTOR_USER: ${TIDB_VECTOR_USER:-}
  TIDB_VECTOR_PASSWORD: ${TIDB_VECTOR_PASSWORD:-}
  TIDB_VECTOR_DATABASE: ${TIDB_VECTOR_DATABASE:-dify}
  ORACLE_HOST: ${ORACLE_HOST:-oracle}
  ORACLE_PORT: ${ORACLE_PORT:-1521}
  ORACLE_USER: ${ORACLE_USER:-dify}
  ORACLE_PASSWORD: ${ORACLE_PASSWORD:-dify}
  ORACLE_DATABASE: ${ORACLE_DATABASE:-FREEPDB1}
  CHROMA_HOST: ${CHROMA_HOST:-127.0.0.1}
  CHROMA_PORT: ${CHROMA_PORT:-8000}
  CHROMA_TENANT: ${CHROMA_TENANT:-default_tenant}
  CHROMA_DATABASE: ${CHROMA_DATABASE:-default_database}
  CHROMA_AUTH_PROVIDER: ${CHROMA_AUTH_PROVIDER:-chromadb.auth.token_authn.TokenAuthClientProvider}
  CHROMA_AUTH_CREDENTIALS: ${CHROMA_AUTH_CREDENTIALS:-}
  OPENSEARCH_HOST: ${OPENSEARCH_HOST:-opensearch}
  OPENSEARCH_PORT: ${OPENSEARCH_PORT:-9200}
  OPENSEARCH_USER: ${OPENSEARCH_USER:-admin}
  OPENSEARCH_PASSWORD: ${OPENSEARCH_PASSWORD:-admin}
  OPENSEARCH_SECURE: ${OPENSEARCH_SECURE:-true}
  TENCENT_VECTOR_DB_URL: ${TENCENT_VECTOR_DB_URL:-http://127.0.0.1}
  TENCENT_VECTOR_DB_API_KEY: ${TENCENT_VECTOR_DB_API_KEY:-dify}
  TENCENT_VECTOR_DB_TIMEOUT: ${TENCENT_VECTOR_DB_TIMEOUT:-30}
  TENCENT_VECTOR_DB_USERNAME: ${TENCENT_VECTOR_DB_USERNAME:-dify}
  TENCENT_VECTOR_DB_DATABASE: ${TENCENT_VECTOR_DB_DATABASE:-dify}
  TENCENT_VECTOR_DB_SHARD: ${TENCENT_VECTOR_DB_SHARD:-1}
  TENCENT_VECTOR_DB_REPLICAS: ${TENCENT_VECTOR_DB_REPLICAS:-2}
  UPLOAD_FILE_SIZE_LIMIT: ${UPLOAD_FILE_SIZE_LIMIT:-15}
  UPLOAD_FILE_BATCH_LIMIT: ${UPLOAD_FILE_BATCH_LIMIT:-5}
  ETL_TYPE: ${ETL_TYPE:-dify}
  UNSTRUCTURED_API_URL: ${UNSTRUCTURED_API_URL:-}
  MULTIMODAL_SEND_IMAGE_FORMAT: ${MULTIMODAL_SEND_IMAGE_FORMAT:-base64}
  UPLOAD_IMAGE_FILE_SIZE_LIMIT: ${UPLOAD_IMAGE_FILE_SIZE_LIMIT:-10}
  SENTRY_DSN: ${API_SENTRY_DSN:-}
  SENTRY_TRACES_SAMPLE_RATE: ${API_SENTRY_TRACES_SAMPLE_RATE:-1.0}
  SENTRY_PROFILES_SAMPLE_RATE: ${API_SENTRY_PROFILES_SAMPLE_RATE:-1.0}
  NOTION_INTEGRATION_TYPE: ${NOTION_INTEGRATION_TYPE:-public}
  NOTION_CLIENT_SECRET: ${NOTION_CLIENT_SECRET:-}
  NOTION_CLIENT_ID: ${NOTION_CLIENT_ID:-}
  NOTION_INTERNAL_SECRET: ${NOTION_INTERNAL_SECRET:-}
  MAIL_TYPE: ${MAIL_TYPE:-resend}
  MAIL_DEFAULT_SEND_FROM: ${MAIL_DEFAULT_SEND_FROM:-}
  SMTP_SERVER: ${SMTP_SERVER:-}
  SMTP_PORT: ${SMTP_PORT:-465}
  SMTP_USERNAME: ${SMTP_USERNAME:-}
  SMTP_PASSWORD: ${SMTP_PASSWORD:-}
  SMTP_USE_TLS: ${SMTP_USE_TLS:-true}
  SMTP_OPPORTUNISTIC_TLS: ${SMTP_OPPORTUNISTIC_TLS:-false}
  RESEND_API_KEY: ${RESEND_API_KEY:-your-resend-api-key}
  RESEND_API_URL: https://api.resend.com
  INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH: ${INDEXING_MAX_SEGMENTATION_TOKENS_LENGTH:-1000}
  INVITE_EXPIRY_HOURS: ${INVITE_EXPIRY_HOURS:-72}
  CODE_EXECUTION_ENDPOINT: ${CODE_EXECUTION_ENDPOINT:-http://sandbox:8194}
  CODE_EXECUTION_API_KEY: ${SANDBOX_API_KEY:-dify-sandbox}
  CODE_MAX_NUMBER: ${CODE_MAX_NUMBER:-9223372036854775807}
  CODE_MIN_NUMBER: ${CODE_MIN_NUMBER:--9223372036854775808}
  CODE_MAX_STRING_LENGTH: ${CODE_MAX_STRING_LENGTH:-80000}
  TEMPLATE_TRANSFORM_MAX_LENGTH: ${TEMPLATE_TRANSFORM_MAX_LENGTH:-80000}
  CODE_MAX_STRING_ARRAY_LENGTH: ${CODE_MAX_STRING_ARRAY_LENGTH:-30}
  CODE_MAX_OBJECT_ARRAY_LENGTH: ${CODE_MAX_OBJECT_ARRAY_LENGTH:-30}
  CODE_MAX_NUMBER_ARRAY_LENGTH: ${CODE_MAX_NUMBER_ARRAY_LENGTH:-1000}
  SSRF_PROXY_HTTP_URL: ${SSRF_PROXY_HTTP_URL:-http://ssrf_proxy:3128}
  SSRF_PROXY_HTTPS_URL: ${SSRF_PROXY_HTTPS_URL:-http://ssrf_proxy:3128}

services:
  # API service
  api:
    image: langgenius/dify-api:0.6.12-fix1
    restart: always
    environment:
      # Use the shared environment variables.
      <<: *shared-api-worker-env
      # Startup mode, 'api' starts the API server.
      MODE: api
    depends_on:
      - db
      - redis
    volumes:
      # Mount the storage directory to the container, for storing user files.
      - ./volumes/app/storage:/app/api/storage
    networks:
      - ssrf_proxy_network
      - default
+   # add extra_hosts to connect Ollama
+   extra_hosts:
+     - host.docker.internal:host-gateway

  # worker service
  # The Celery worker for processing the queue.
  worker:
    image: langgenius/dify-api:0.6.12-fix1
    restart: always
    environment:
      # Use the shared environment variables.
      <<: *shared-api-worker-env
      # Startup mode, 'worker' starts the Celery worker for processing the queue.
      MODE: worker
    depends_on:
      - db
      - redis
    volumes:
      # Mount the storage directory to the container, for storing user files.
      - ./volumes/app/storage:/app/api/storage
    networks:
      - ssrf_proxy_network
      - default
+   # add extra_hosts to connect Ollama
+   extra_hosts:
+     - host.docker.internal:host-gateway

  # Frontend web application.
  web:
    image: langgenius/dify-web:0.6.12-fix1
    restart: always
    environment:
      CONSOLE_API_URL: ${CONSOLE_API_URL:-}
      APP_API_URL: ${APP_API_URL:-}
      SENTRY_DSN: ${WEB_SENTRY_DSN:-}
+   # add extra_hosts to connect Ollama
+   extra_hosts:
+     - host.docker.internal:host-gateway

  # The postgres database.
  db:
    image: postgres:15-alpine
    restart: always
    environment:
      PGUSER: ${PGUSER:-postgres}
      POSTGRES_PASSWORD: ${POSTGRES_PASSWORD:-difyai123456}
      POSTGRES_DB: ${POSTGRES_DB:-dify}
      PGDATA: ${PGDATA:-/var/lib/postgresql/data/pgdata}
    volumes:
      - ./volumes/db/data:/var/lib/postgresql/data
    healthcheck:
      test: [ "CMD", "pg_isready" ]
      interval: 1s
      timeout: 3s
      retries: 30
+   # add extra_hosts to connect Ollama
+   extra_hosts:
+     - host.docker.internal:host-gateway

  # The redis cache.
  redis:
    image: redis:6-alpine
    restart: always
    volumes:
      # Mount the redis data directory to the container.
      - ./volumes/redis/data:/data
    # Set the redis password when startup redis server.
    command: redis-server --requirepass ${REDIS_PASSWORD:-difyai123456}
    healthcheck:
      test: [ "CMD", "redis-cli", "ping" ]
+   # add extra_hosts to connect Ollama
+   extra_hosts:
+     - host.docker.internal:host-gateway

  # The DifySandbox
  sandbox:
    image: langgenius/dify-sandbox:0.2.1
    restart: always
    environment:
      # The DifySandbox configurations
      # Make sure you are changing this key for your deployment with a strong key.
      # You can generate a strong key using `openssl rand -base64 42`.
      API_KEY: ${SANDBOX_API_KEY:-dify-sandbox}
      GIN_MODE: ${SANDBOX_GIN_MODE:-release}
      WORKER_TIMEOUT: ${SANDBOX_WORKER_TIMEOUT:-15}
      ENABLE_NETWORK: ${SANDBOX_ENABLE_NETWORK:-true}
      HTTP_PROXY: ${SANDBOX_HTTP_PROXY:-http://ssrf_proxy:3128}
      HTTPS_PROXY: ${SANDBOX_HTTPS_PROXY:-http://ssrf_proxy:3128}
      SANDBOX_PORT: ${SANDBOX_PORT:-8194}
    volumes:
      - ./volumes/sandbox/dependencies:/dependencies
    networks:
      - ssrf_proxy_network
+   # add extra_hosts to connect Ollama
+   extra_hosts:
+     - host.docker.internal:host-gateway

  # ssrf_proxy server
  # for more information, please refer to
  # https://docs.dify.ai/getting-started/install-self-hosted/install-faq#id-16.-why-is-ssrf_proxy-needed
  ssrf_proxy:
    image: ubuntu/squid:latest
    restart: always
    volumes:
      - ./ssrf_proxy/squid.conf.template:/etc/squid/squid.conf.template
      - ./ssrf_proxy/docker-entrypoint.sh:/docker-entrypoint-mount.sh
    entrypoint: [ "sh", "-c", "cp /docker-entrypoint-mount.sh /docker-entrypoint.sh && sed -i 's/\r$$//' /docker-entrypoint.sh && chmod +x /docker-entrypoint.sh && /docker-entrypoint.sh" ]
    environment:
      # pls clearly modify the squid env vars to fit your network environment.
      HTTP_PORT: ${SSRF_HTTP_PORT:-3128}
      COREDUMP_DIR: ${SSRF_COREDUMP_DIR:-/var/spool/squid}
      REVERSE_PROXY_PORT: ${SSRF_REVERSE_PROXY_PORT:-8194}
      SANDBOX_HOST: ${SSRF_SANDBOX_HOST:-sandbox}
      SANDBOX_PORT: ${SANDBOX_PORT:-8194}
    networks:
      - ssrf_proxy_network
      - default
+   # add extra_hosts to connect Ollama
+   extra_hosts:
+     - host.docker.internal:host-gateway

  # The nginx reverse proxy.
  # used for reverse proxying the API service and Web service.
  nginx:
    image: nginx:latest
    restart: always
    volumes:
      - ./nginx/nginx.conf.template:/etc/nginx/nginx.conf.template
      - ./nginx/proxy.conf.template:/etc/nginx/proxy.conf.template
      - ./nginx/https.conf.template:/etc/nginx/https.conf.template
      - ./nginx/conf.d:/etc/nginx/conf.d
      - ./nginx/docker-entrypoint.sh:/docker-entrypoint-mount.sh
      - ./nginx/ssl:/etc/ssl
    entrypoint: [ "sh", "-c", "cp /docker-entrypoint-mount.sh /docker-entrypoint.sh && sed -i 's/\r$$//' /docker-entrypoint.sh && chmod +x /docker-entrypoint.sh && /docker-entrypoint.sh" ]
    environment:
      NGINX_SERVER_NAME: ${NGINX_SERVER_NAME:-_}
      NGINX_HTTPS_ENABLED: ${NGINX_HTTPS_ENABLED:-false}
      NGINX_SSL_PORT: ${NGINX_SSL_PORT:-443}
      NGINX_PORT: ${NGINX_PORT:-80}
      # You're required to add your own SSL certificates/keys to the `./nginx/ssl` directory
      # and modify the env vars below in .env if HTTPS_ENABLED is true.
      NGINX_SSL_CERT_FILENAME: ${NGINX_SSL_CERT_FILENAME:-dify.crt}
      NGINX_SSL_CERT_KEY_FILENAME: ${NGINX_SSL_CERT_KEY_FILENAME:-dify.key}
      NGINX_SSL_PROTOCOLS: ${NGINX_SSL_PROTOCOLS:-TLSv1.1 TLSv1.2 TLSv1.3}
      NGINX_WORKER_PROCESSES: ${NGINX_WORKER_PROCESSES:-auto}
      NGINX_CLIENT_MAX_BODY_SIZE: ${NGINX_CLIENT_MAX_BODY_SIZE:-15M}
      NGINX_KEEPALIVE_TIMEOUT: ${NGINX_KEEPALIVE_TIMEOUT:-65}
      NGINX_PROXY_READ_TIMEOUT: ${NGINX_PROXY_READ_TIMEOUT:-3600s}
      NGINX_PROXY_SEND_TIMEOUT: ${NGINX_PROXY_SEND_TIMEOUT:-3600s}
    depends_on:
      - api
      - web
    ports:
      - "${EXPOSE_NGINX_PORT:-80}:${NGINX_PORT:-80}"
      - "${EXPOSE_NGINX_SSL_PORT:-443}:${NGINX_SSL_PORT:-443}"
+   # add extra_hosts to connect Ollama
+   extra_hosts:
+     - host.docker.internal:host-gateway

  # The Weaviate vector store.
  weaviate:
    image: semitechnologies/weaviate:1.19.0
    profiles:
      - ''
      - weaviate
    restart: always
    volumes:
      # Mount the Weaviate data directory to the con tainer.
      - ./volumes/weaviate:/var/lib/weaviate
    environment:
      # The Weaviate configurations
      # You can refer to the [Weaviate](https://weaviate.io/developers/weaviate/config-refs/env-vars) documentation for more information.
      PERSISTENCE_DATA_PATH: ${WEAVIATE_PERSISTENCE_DATA_PATH:-/var/lib/weaviate}
      QUERY_DEFAULTS_LIMIT: ${WEAVIATE_QUERY_DEFAULTS_LIMIT:-25}
      AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: ${WEAVIATE_AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED:-false}
      DEFAULT_VECTORIZER_MODULE: ${WEAVIATE_DEFAULT_VECTORIZER_MODULE:-none}
      CLUSTER_HOSTNAME: ${WEAVIATE_CLUSTER_HOSTNAME:-node1}
      AUTHENTICATION_APIKEY_ENABLED: ${WEAVIATE_AUTHENTICATION_APIKEY_ENABLED:-true}
      AUTHENTICATION_APIKEY_ALLOWED_KEYS: ${WEAVIATE_AUTHENTICATION_APIKEY_ALLOWED_KEYS:-WVF5YThaHlkYwhGUSmCRgsX3tD5ngdN8pkih}
      AUTHENTICATION_APIKEY_USERS: ${WEAVIATE_AUTHENTICATION_APIKEY_USERS:-hello@dify.ai}
      AUTHORIZATION_ADMINLIST_ENABLED: ${WEAVIATE_AUTHORIZATION_ADMINLIST_ENABLED:-true}
      AUTHORIZATION_ADMINLIST_USERS: ${WEAVIATE_AUTHORIZATION_ADMINLIST_USERS:-hello@dify.ai}
+   # add extra_hosts to connect Ollama
+   extra_hosts:
+     - host.docker.internal:host-gateway

  # Qdrant vector store.
  # (if used, you need to set VECTOR_STORE to qdrant in the api & worker service.)
  qdrant:
    image: langgenius/qdrant:v1.7.3
    profiles:
      - qdrant
    restart: always
    volumes:
      - ./volumes/qdrant:/qdrant/storage
    environment:
      QDRANT_API_KEY: ${QDRANT_API_KEY:-difyai123456}
+   # add extra_hosts to connect Ollama
+   extra_hosts:
+     - host.docker.internal:host-gateway

  # The pgvector vector database.
  pgvector:
    image: pgvector/pgvector:pg16
    profiles:
      - pgvector
    restart: always
    environment:
      PGUSER: ${PGVECTOR_PGUSER:-postgres}
      # The password for the default postgres user.
      POSTGRES_PASSWORD: ${PGVECTOR_POSTGRES_PASSWORD:-difyai123456}
      # The name of the default postgres database.
      POSTGRES_DB: ${PGVECTOR_POSTGRES_DB:-dify}
      # postgres data directory
      PGDATA: ${PGVECTOR_PGDATA:-/var/lib/postgresql/data/pgdata}
    volumes:
      - ./volumes/pgvector/data:/var/lib/postgresql/data
    healthcheck:
      test: [ "CMD", "pg_isready" ]
      interval: 1s
      timeout: 3s
      retries: 30
+   # add extra_hosts to connect Ollama
+   extra_hosts:
+     - host.docker.internal:host-gateway

  # pgvecto-rs vector store
  pgvecto-rs:
    image: tensorchord/pgvecto-rs:pg16-v0.2.0
    profiles:
      - pgvecto-rs
    restart: always
    environment:
      PGUSER: ${PGVECTOR_PGUSER:-postgres}
      # The password for the default postgres user.
      POSTGRES_PASSWORD: ${PGVECTOR_POSTGRES_PASSWORD:-difyai123456}
      # The name of the default postgres database.
      POSTGRES_DB: ${PGVECTOR_POSTGRES_DB:-dify}
      # postgres data directory
      PGDATA: ${PGVECTOR_PGDATA:-/var/lib/postgresql/data/pgdata}
    volumes:
      - ./volumes/pgvecto_rs/data:/var/lib/postgresql/data
    healthcheck:
      test: [ "CMD", "pg_isready" ]
      interval: 1s
      timeout: 3s
      retries: 30
+   # add extra_hosts to connect Ollama
+   extra_hosts:
+     - host.docker.internal:host-gateway

  # Chroma vector database
  chroma:
    image: ghcr.io/chroma-core/chroma:0.5.1
    profiles:
      - chroma
    restart: always
    volumes:
      - ./volumes/chroma:/chroma/chroma
    environment:
      CHROMA_SERVER_AUTHN_CREDENTIALS: ${CHROMA_SERVER_AUTHN_CREDENTIALS:-difyai123456}
      CHROMA_SERVER_AUTHN_PROVIDER: ${CHROMA_SERVER_AUTHN_PROVIDER:-chromadb.auth.token_authn.TokenAuthenticationServerProvider}
      IS_PERSISTENT: ${CHROMA_IS_PERSISTENT:-TRUE}
+   # add extra_hosts to connect Ollama
+   extra_hosts:
+     - host.docker.internal:host-gateway

  # Oracle vector database
  oracle:
    image: container-registry.oracle.com/database/free:latest
    profiles:
      - oracle
    restart: always
    volumes:
      - type: volume
        source: oradata
        target: /opt/oracle/oradata
      - ./startupscripts:/opt/oracle/scripts/startup
    environment:
      - ORACLE_PWD=${ORACLE_PWD:-Dify123456}
      - ORACLE_CHARACTERSET=${ORACLE_CHARACTERSET:-AL32UTF8}
+   # add extra_hosts to connect Ollama
+   extra_hosts:
+     - host.docker.internal:host-gateway

  # Milvus vector database services
  etcd:
    container_name: milvus-etcd
    image: quay.io/coreos/etcd:v3.5.5
    profiles:
      - milvus
    environment:
      - ETCD_AUTO_COMPACTION_MODE=${ETCD_AUTO_COMPACTION_MODE:-revision}
      - ETCD_AUTO_COMPACTION_RETENTION=${ETCD_AUTO_COMPACTION_RETENTION:-1000}
      - ETCD_QUOTA_BACKEND_BYTES=${ETCD_QUOTA_BACKEND_BYTES:-4294967296}
      - ETCD_SNAPSHOT_COUNT=${ETCD_SNAPSHOT_COUNT:-50000}
    volumes:
      - ./volumes/milvus/etcd:/etcd
    command: etcd -advertise-client-urls=http://127.0.0.1:2379 -listen-client-urls http://0.0.0.0:2379 --data-dir /etcd
    healthcheck:
      test: ["CMD", "etcdctl", "endpoint", "health"]
      interval: 30s
      timeout: 20s
      retries: 3
    networks:
      - milvus
+   # add extra_hosts to connect Ollama
+   extra_hosts:
+     - host.docker.internal:host-gateway

  minio:
    container_name: milvus-minio
    image: minio/minio:RELEASE.2023-03-20T20-16-18Z
    profiles:
      - milvus
    environment:
      MINIO_ACCESS_KEY: ${MINIO_ACCESS_KEY:-minioadmin}
      MINIO_SECRET_KEY: ${MINIO_SECRET_KEY:-minioadmin}
    volumes:
      - ./volumes/milvus/minio:/minio_data
    command: minio server /minio_data --console-address ":9001"
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9000/minio/health/live"]
      interval: 30s
      timeout: 20s
      retries: 3
    networks:
      - milvus
+   # add extra_hosts to connect Ollama
+   extra_hosts:
+     - host.docker.internal:host-gateway

  milvus-standalone:
    container_name: milvus-standalone
    image: milvusdb/milvus:v2.3.1
    profiles:
      - milvus
    command: ["milvus", "run", "standalone"]
    environment:
      ETCD_ENDPOINTS: ${ETCD_ENDPOINTS:-etcd:2379}
      MINIO_ADDRESS: ${MINIO_ADDRESS:-minio:9000}
      common.security.authorizationEnabled: ${MILVUS_AUTHORIZATION_ENABLED:-true}
    volumes:
      - ./volumes/milvus/milvus:/var/lib/milvus
    healthcheck:
      test: ["CMD", "curl", "-f", "http://localhost:9091/healthz"]
      interval: 30s
      start_period: 90s
      timeout: 20s
      retries: 3
    depends_on:
      - "etcd"
      - "minio"
    networks:
      - milvus
+   # add extra_hosts to connect Ollama
+   extra_hosts:
+     - host.docker.internal:host-gateway

  # Opensearch vector database
  opensearch:
    container_name: opensearch
    image: opensearchproject/opensearch:latest
    profiles:
      - opensearch
    environment:
      - discovery.type=${OPENSEARCH_DISCOVERY_TYPE:-single-node}
      - bootstrap.memory_lock=${OPENSEARCH_BOOTSTRAP_MEMORY_LOCK:-true}
      - OPENSEARCH_JAVA_OPTS=-Xms${OPENSEARCH_JAVA_OPTS_MIN:-512m} -Xmx${OPENSEARCH_JAVA_OPTS_MAX:-1024m}
      - OPENSEARCH_INITIAL_ADMIN_PASSWORD=${OPENSEARCH_INITIAL_ADMIN_PASSWORD:-Qazwsxedc!@#123}
    ulimits:
      memlock:
        soft: ${OPENSEARCH_MEMLOCK_SOFT:--1}
        hard: ${OPENSEARCH_MEMLOCK_HARD:--1}
      nofile:
        soft: ${OPENSEARCH_NOFILE_SOFT:-65536}
        hard: ${OPENSEARCH_NOFILE_HARD:-65536}
    volumes:
      - ./volumes/opensearch/data:/usr/share/opensearch/data
    networks:
      - opensearch-net
+   # add extra_hosts to connect Ollama
+   extra_hosts:
+     - host.docker.internal:host-gateway

  opensearch-dashboards:
    container_name: opensearch-dashboards
    image: opensearchproject/opensearch-dashboards:latest
    profiles:
      - opensearch
    environment:
      OPENSEARCH_HOSTS: '["https://opensearch:9200"]'
    volumes:
      - ./volumes/opensearch/opensearch_dashboards.yml:/usr/share/opensearch-dashboards/config/opensearch_dashboards.yml
    networks:
      - opensearch-net
    depends_on:
      - opensearch
+   # add extra_hosts to connect Ollama
+   extra_hosts:
+     - host.docker.internal:host-gateway

networks:
  # create a network between sandbox, api and ssrf_proxy, and can not access outside.
  ssrf_proxy_network:
    driver: bridge
    internal: true
  milvus:
    driver: bridge
  opensearch-net:
    driver: bridge
    internal: true

volumes:
  oradata:

```
</details>


:::note
とりあえず全てのサービスの配下に `extra_hosts` を追記して動作確認までしましたが、改めて考えると、全てに記載する必要はなかったかなと思います。。
こちらの記事では `api` 配下にのみ `extra_hosts` を追記しているようです。
（出典）[Dify(docker compose)+Ollama(docker)を試す。](https://bwgift.hatenadiary.jp/entry/2024/05/12/232531)
:::

以下のコマンドでDifyを起動し直します。
```
$ docker compose up -d
```

再度Difyにサインインします。
Difyへモデルを追加する際は、以下のように、`Base URL` に `http://host.docker.internal:11434` を入れます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/731275/b49c0b5f-5f0f-6768-85d8-c900bc266378.png)


## DifyからS3を利用できるようにする

EC2でDifyを構築しましたが、ストレージとしては当然EBSが利用されます。
RAG目的でサイズが大きなドキュメントをアップロードすると、ストレージの空き容量がゴリゴリ削られていきます。
```
EBSは拡張も面倒だし、お値段もそれなりだし、、
あ～～～ S3使いたい～～～！！！
```

DifyとS3を連携させ、スケーラブルかつ安価なストレージを利用する方法は以下の記事にまとめましたので、是非ご参照ください。

https://qiita.com/yuki_ink/items/fbb83734fbaa4b03b9f4

# 終わりに
ローカルPCでDifyを動かしていた時に比べ、格段に処理が早くなりました！
EC2という「枯れた技術」のみを利用することの安心感があり、当然スケーラビリティもあり、意外と需要があるのではと思います。
よければお試しください！

今後は、以下の2つに取り組んでいきたいと思います。
- HTTPS対応したい！
- VPCエンドポイント経由でBedrockを使いたい！

最後までお目通しいただき、ありがとうございました。