🌀

Difyの.env.exampleを毎回目視で見てる人へ:安全に同期するスクリプトを書いた

に公開

はじめに

OSS 版の Dify を本番環境で運用していると、バージョンアップのたびに必ず発生する作業があります。それが .env.example の変更内容を既存の .env に反映する作業 です。

Dify の .env.example は 1400 行を超える非常に大きなファイルで、バージョンが上がるたびに新しい環境変数の追加や、既存設定の推奨値変更、非推奨項目の整理などが行われます。
これを毎回目視で確認し、本番環境用に調整された .env に反映するのは、正直かなりの負担です。

実際の運用では、次のような悩みを感じていました。

  • 新しい環境変数が追加されているが、見落としてしまう
  • 推奨値が変わっているが、本番では変えるべきか判断に迷う
  • どこまでが「安全に自動反映してよい変更」なのか分からない
  • うっかり本番固有の設定を上書きしてしまいそうで怖い

本記事では、こうした課題を解決するために作成した
「部分同期」に対応した環境変数同期シェルスクリプト を紹介します。

このスクリプトを使うことで以下の運用が可能になります。

  • 本番環境のカスタム設定を保持したまま
  • .env.example に追加された新規変数だけを安全に取り込み
  • 変更差分を分かりやすく可視化する

想定読者

この記事は、以下のような方を想定しています。

  • OSS 版 Dify を Docker 環境で運用している方
  • バージョンアップ作業をできるだけ安全・効率的にしたい方

「Dify の env 管理、正直しんどい…」と感じたことがある方には、特に刺さる内容になっていると思います。

解決策の概要

今回作成した dify-env-sync.sh は、Dify の .env.example.env を比較しながら、必要な変更だけを安全に反映する ことを目的としたスクリプトです。

dify-env-sync.sh
#!/bin/bash

# ================================================================
# Dify環境変数同期スクリプト
#
# 機能:
# - .env.exampleの最新設定を.envに同期
# - my.envに記載された独自設定値を保持
# - 新しい環境変数の追加
# - 削除された環境変数の検出
# - バックアップファイルの作成
# ================================================================

# set -e  # エラー時に即座に終了(デバッグのため一時的にコメントアウト)

# エラーハンドリング関数
handle_error() {
    local line_no=$1
    local error_code=$2
    echo -e "\033[0;31m[ERROR]\033[0m スクリプトエラー: 行 $line_no でエラーコード $error_code"
    echo -e "\033[0;31m[ERROR]\033[0m デバッグ情報: 現在の作業ディレクトリ $(pwd)"
    exit $error_code
}

# エラートラップの設定
trap 'handle_error ${LINENO} $?' ERR

# 色付き出力のための設定
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color

# ログ関数
log_info() {
    echo -e "${BLUE}[INFO]${NC} $1"
}

log_success() {
    echo -e "${GREEN}[SUCCESS]${NC} $1"
}

log_warning() {
    echo -e "${YELLOW}[WARNING]${NC} $1"
}

log_error() {
    echo -e "${RED}[ERROR]${NC} $1"
}

# ファイル存在チェック
check_files() {
    log_info "必要なファイルの存在確認中..."

    if [[ ! -f ".env.example" ]]; then
        log_error ".env.example が見つかりません"
        exit 1
    fi

    if [[ ! -f ".env" ]]; then
        log_warning ".env ファイルが存在しません。.env.example をコピーして作成します。"
        cp ".env.example" ".env"
        log_success ".env ファイルを作成しました"
    fi

    log_success "必要なファイルが確認されました"
}

# バックアップファイル作成
create_backup() {
    local timestamp=$(date +"%Y%m%d_%H%M%S")
    local backup_dir="env-backup"

    # バックアップディレクトリが存在しない場合は作成
    if [[ ! -d "$backup_dir" ]]; then
        mkdir -p "$backup_dir"
        log_info "バックアップディレクトリ $backup_dir を作成しました"
    fi

    if [[ -f ".env" ]]; then
        local backup_file="${backup_dir}/.env.backup_${timestamp}"
        cp ".env" "$backup_file"
        log_success "既存の .env を $backup_file にバックアップしました"
    fi
}

# .envと.env.exampleの差分を検出
detect_differences() {
    log_info ".env と .env.example の差分を検出中..."

    local temp_env="/tmp/env_values_$$"
    local temp_example="/tmp/example_values_$$"

    # .envの値を取得
    > "$temp_env"
    while read -r line; do
        local parsed=$(parse_env_line "$line")
        if [[ $? -eq 0 ]]; then
            local key=$(echo "$parsed" | cut -d'|' -f1)
            local value=$(echo "$parsed" | cut -d'|' -f2-)
            echo "${key}|${value}" >> "$temp_env"
        fi
    done < .env

    # .env.exampleの値を取得
    > "$temp_example"
    while read -r line; do
        local parsed=$(parse_env_line "$line")
        if [[ $? -eq 0 ]]; then
            local key=$(echo "$parsed" | cut -d'|' -f1)
            local value=$(echo "$parsed" | cut -d'|' -f2-)
            echo "${key}|${value}" >> "$temp_example"
        fi
    done < .env.example

    # 一時的に差分情報を格納するファイル
    local temp_diff="/tmp/env_diff_$$"
    > "$temp_diff"

    # 差分を検出
    local diff_count=0
    while read -r example_line; do
        local key=$(echo "$example_line" | cut -d'|' -f1)
        local example_value=$(echo "$example_line" | cut -d'|' -f2-)

        # .envから対応する値を取得
        local env_value=$(grep "^${key}|" "$temp_env" 2>/dev/null | cut -d'|' -f2- || echo "")

        if [[ -n "$env_value" && "$env_value" != "$example_value" ]]; then
            echo "${key}|${env_value}" >> "$temp_diff"
            ((diff_count++))
        fi
    done < "$temp_example"

    # グローバル変数として差分ファイルパスを保存
    declare -g DIFF_FILE="$temp_diff"

    # 一時ファイルをクリーンアップ
    rm -f "$temp_env" "$temp_example"

    if [[ $diff_count -gt 0 ]]; then
        log_success "$diff_count 個の環境変数で差分が検出されました"

        # 差分の詳細を表示
        show_differences_detail
    else
        log_info "差分は検出されませんでした"
    fi
}

# 環境変数のパース関数
parse_env_line() {
    local line="$1"
    local key=""
    local value=""

    # 空行やコメント行をスキップ
    [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && return 1

    # = で分割
    if [[ "$line" =~ ^([^=]+)=(.*)$ ]]; then
        key="${BASH_REMATCH[1]}"
        value="${BASH_REMATCH[2]}"

        # 先頭・末尾の空白を除去
        key=$(echo "$key" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')
        value=$(echo "$value" | sed 's/^[[:space:]]*//; s/[[:space:]]*$//')

        if [[ -n "$key" ]]; then
            echo "$key|$value"
            return 0
        fi
    fi

    return 1
}

# 差分の詳細を表示
show_differences_detail() {
    log_info ""
    log_info "=== 環境変数の差分詳細 ==="

    # 一時ファイルを再作成
    local temp_env="/tmp/env_values_detail_$$"
    local temp_example="/tmp/example_values_detail_$$"

    # .envの値を取得
    > "$temp_env"
    while read -r line; do
        local parsed=$(parse_env_line "$line")
        if [[ $? -eq 0 ]]; then
            local key=$(echo "$parsed" | cut -d'|' -f1)
            local value=$(echo "$parsed" | cut -d'|' -f2-)
            echo "${key}|${value}" >> "$temp_env"
        fi
    done < .env

    # .env.exampleの値を取得
    > "$temp_example"
    while read -r line; do
        local parsed=$(parse_env_line "$line")
        if [[ $? -eq 0 ]]; then
            local key=$(echo "$parsed" | cut -d'|' -f1)
            local value=$(echo "$parsed" | cut -d'|' -f2-)
            echo "${key}|${value}" >> "$temp_example"
        fi
    done < .env.example

    # 差分を表示
    local count=1
    while read -r example_line; do
        local key=$(echo "$example_line" | cut -d'|' -f1)
        local example_value=$(echo "$example_line" | cut -d'|' -f2-)

        # .envから対応する値を取得
        local env_value=$(grep "^${key}|" "$temp_env" 2>/dev/null | cut -d'|' -f2- || echo "")

        if [[ -n "$env_value" && "$env_value" != "$example_value" ]]; then
            echo ""
            echo -e "${YELLOW}[$count] $key${NC}"
            echo -e "  ${GREEN}.env (現在値)${NC}     : ${env_value}"
            echo -e "  ${BLUE}.env.example (推奨値)${NC}: ${example_value}"

            # 値の比較分析
            analyze_value_change "$env_value" "$example_value"
            ((count++))
        fi
    done < "$temp_example"

    # 一時ファイルをクリーンアップ
    rm -f "$temp_env" "$temp_example"

    echo ""
    log_info "=== 差分詳細表示完了 ==="
    log_info "注意: 上記の推奨値への変更を検討してください。"
    log_info "現在の実装では .env の値を保持します。"
    echo ""
}

# 値の変更分析
analyze_value_change() {
    local current_value="$1"
    local recommended_value="$2"

    # 値の特徴を分析
    local analysis=""

    # 空値チェック
    if [[ -z "$current_value" && -n "$recommended_value" ]]; then
        analysis="  ${RED}→ 空値から推奨値への設定${NC}"
    elif [[ -n "$current_value" && -z "$recommended_value" ]]; then
        analysis="  ${RED}→ 推奨値が空値に変更${NC}"
    # 数値チェック
    elif [[ "$current_value" =~ ^[0-9]+$ && "$recommended_value" =~ ^[0-9]+$ ]]; then
        if [[ $current_value -lt $recommended_value ]]; then
            analysis="  ${BLUE}→ 数値増加 (${current_value} < ${recommended_value})${NC}"
        elif [[ $current_value -gt $recommended_value ]]; then
            analysis="  ${YELLOW}→ 数値減少 (${current_value} > ${recommended_value})${NC}"
        fi
    # ブール値チェック
    elif [[ "$current_value" =~ ^(true|false)$ && "$recommended_value" =~ ^(true|false)$ ]]; then
        if [[ "$current_value" != "$recommended_value" ]]; then
            analysis="  ${BLUE}→ ブール値変更 (${current_value}${recommended_value})${NC}"
        fi
    # URL/エンドポイントチェック
    elif [[ "$current_value" =~ ^https?:// || "$recommended_value" =~ ^https?:// ]]; then
        analysis="  ${BLUE}→ URL/エンドポイント変更${NC}"
    # ファイルパスチェック
    elif [[ "$current_value" =~ ^/ || "$recommended_value" =~ ^/ ]]; then
        analysis="  ${BLUE}→ ファイルパス変更${NC}"
    else
        # 長さの比較
        local current_len=${#current_value}
        local recommended_len=${#recommended_value}
        if [[ $current_len -ne $recommended_len ]]; then
            analysis="  ${YELLOW}→ 文字列長変更 (${current_len}${recommended_len} 文字)${NC}"
        fi
    fi

    if [[ -n "$analysis" ]]; then
        echo -e "$analysis"
    fi
}

# .envファイルの部分同期
sync_env_file() {
    log_info ".env ファイルの部分同期を開始..."

    local new_env_file=".env.new"
    local preserved_count=0
    local updated_count=0

    # 新しい.envファイルを作成
    > "$new_env_file"

    # .env.exampleを基準に処理
    while read -r line; do
        # コメント行と空行はそのまま保持
        if [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]]; then
            echo "$line" >> "$new_env_file"
            continue
        fi

        local parsed=$(parse_env_line "$line")
        if [[ $? -eq 0 ]]; then
            local key=$(echo "$parsed" | cut -d'|' -f1)
            local example_value=$(echo "$parsed" | cut -d'|' -f2-)
            local final_value="$example_value"

            # 差分ファイルから.envの値があるかチェック
            if [[ -f "$DIFF_FILE" ]]; then
                local env_value=$(grep "^${key}|" "$DIFF_FILE" 2>/dev/null | cut -d'|' -f2- || echo "")
                if [[ -n "$env_value" ]]; then
                    final_value="$env_value"
                    log_info "  保持: $key (.env値)"
                    ((preserved_count++))
                else
                    log_info "  更新: $key (.env.example値)"
                    ((updated_count++))
                fi
            else
                log_info "  デフォルト: $key (.env.example値)"
                ((updated_count++))
            fi

            # 最終的な行を出力
            echo "${key}=${final_value}" >> "$new_env_file"
        else
            # パースできなかった行はそのまま保持
            echo "$line" >> "$new_env_file"
        fi
    done < .env.example

    # 新しいファイルを既存ファイルと置き換え
    mv "$new_env_file" ".env"

    # 差分ファイルをクリーンアップ
    rm -f "$DIFF_FILE"

    log_success ".env ファイルの部分同期が完了しました"
    log_info "  保持された.env値: $preserved_count"
    log_info "  .env.example値に更新: $updated_count"
}

# 削除された環境変数を検出
detect_removed_variables() {
    log_info "削除された環境変数を検出中..."

    if [[ ! -f ".env" ]]; then
        return
    fi

    # 一時ファイルを使用してキーを管理
    local temp_example="/tmp/example_keys_$$"
    local temp_current="/tmp/current_keys_$$"

    # .env.exampleのキーを取得
    > "$temp_example"
    while read -r line; do
        local parsed=$(parse_env_line "$line")
        if [[ $? -eq 0 ]]; then
            local key=$(echo "$parsed" | cut -d'|' -f1)
            echo "$key" >> "$temp_example"
        fi
    done < .env.example

    # 既存の.envのキーを取得
    > "$temp_current"
    while read -r line; do
        local parsed=$(parse_env_line "$line")
        if [[ $? -eq 0 ]]; then
            local key=$(echo "$parsed" | cut -d'|' -f1)
            echo "$key" >> "$temp_current"
        fi
    done < .env

    # 削除された変数を検出
    local removed_vars=()
    while read -r key; do
        if ! grep -q "^${key}$" "$temp_example" 2>/dev/null; then
            removed_vars+=("$key")
        fi
    done < "$temp_current"

    # 一時ファイルをクリーンアップ
    rm -f "$temp_example" "$temp_current"

    if [[ ${#removed_vars[@]} -gt 0 ]]; then
        log_warning "以下の環境変数が .env.example から削除されています:"
        for var in "${removed_vars[@]}"; do
            log_warning "  - $var"
        done
        log_warning "これらの変数を .env から手動で削除することを検討してください"
    else
        log_success "削除された環境変数はありません"
    fi
}

# 統計情報表示
show_statistics() {
    log_info "同期結果の統計:"

    local total_example=$(grep -c "^[^#]*=" .env.example 2>/dev/null || echo "0")
    local total_env=$(grep -c "^[^#]*=" .env 2>/dev/null || echo "0")

    log_info "  .env.example の環境変数: $total_example"
    log_info "  .env の環境変数: $total_env"
}

# メイン実行関数
main() {
    log_info "=== Dify環境変数部分同期スクリプト ==="
    log_info "実行開始: $(date)"

    # 前提条件チェック
    check_files

    # バックアップ作成
    create_backup

    # 差分検出
    detect_differences

    # 環境ファイル部分同期
    sync_env_file

    # 削除された変数の検出
    detect_removed_variables

    # 統計情報表示
    show_statistics

    log_success "=== 部分同期処理が正常に完了しました ==="
    log_info "終了時刻: $(date)"
}

# スクリプトが直接実行された場合のみmain関数を実行
if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then
    main "$@"
fi

主要な特徴

このスクリプトには、以下のような特徴があります。

  1. 部分同期

    • 既存の .env に設定されている値は原則保持
    • .env.example に新しく追加された変数のみ自動追加
  2. 詳細な差分表示

    • 単なる「違い」ではなく、数値・ブール値・URL などの変更内容を分析して表示
  3. 自動バックアップ

    • 実行前に必ず .env を日時付きでバックアップ
  4. 安全性重視

    • 本番環境の設定を意図せず上書きしない設計

「全部自動で書き換える」のではなく、
判断が必要な部分は人が確認しやすい形で残す という方針で作っています。

動作イメージ

処理の流れはシンプルです。

  1. .env.example.env を読み込む
  2. 環境変数ごとの差分を検出・分析
  3. 実行前に .env をバックアップ
  4. 既存の設定は保持しつつ、新規変数のみを .env に追加
  5. 更新後の .env を生成

「安全に差分を取り込みたい」という運用ニーズを、そのままスクリプトに落とし込んだ形です。

背景と課題

Dify の環境変数管理の複雑さ

Dify は非常に柔軟で高機能な OSS である一方、環境変数の数もかなり多くなっています。
データベース、Redis、ストレージ、外部 API、ワーカー設定、セキュリティ関連など、ほぼすべてが環境変数で制御されています。

一部を抜粋すると、次のような項目です。

DB_TYPE=postgresql
REDIS_HOST=redis
STORAGE_TYPE=opendal
UPLOAD_FILE_SIZE_LIMIT=15
VECTOR_STORE=weaviate

これらが 1000 行以上並ぶため、差分を人間が正確に把握するのは現実的ではありません

手動同期の問題点

これまで手動で同期していた際、特に問題になっていたのは次の点です。

  1. 作業時間がかかる
    差分確認だけで 1〜2 時間かかることも珍しくありません。

  2. ヒューマンエラーが起きやすい
    新規変数の見落としや、不要な上書きが発生しがちです。

  3. 本番設定との衝突
    開発用デフォルト値が、本番用の調整済み設定を壊してしまうリスクがあります。

  4. 変更履歴が追いづらい
    どの変数が「今回のバージョンで変わったのか」が分かりにくい状態でした。

このままでは、バージョンアップのたびに心理的な負担が大きくなってしまいます。

実装

ファイル構成

スクリプトは Dify の docker ディレクトリ配下に配置する想定です。

./dify/docker/
├── .env.example
├── .env
├── dify-env-sync.sh
├── env-backup/
│   └── .env.backup_YYYYMMDD_HHMMSS
└── docker-compose.yaml

.env.example.env が同じディレクトリにある前提にすることで、
余計な設定なしに実行できるようにしています。

環境変数の解析

スクリプトでは、.env ファイルを 1 行ずつ解析し、
KEY=VALUE 形式のものだけを対象に処理します。

parse_env_line() {
    local line="$1"
    [[ -z "$line" || "$line" =~ ^[[:space:]]*# ]] && return 1

    if [[ "$line" =~ ^([^=]+)=(.*)$ ]]; then
        key="${BASH_REMATCH[1]}"
        value="${BASH_REMATCH[2]}"
        echo "$key|$value"
        return 0
    fi
    return 1
}

コメント行や空行は無視し、
「環境変数として意味のある行」だけを安全に扱うようにしています。

差分検出と分析

単純な文字列比較だけでなく、
値の種類に応じた差分分析 を行っているのがポイントです。

analyze_value_change() {
    if [[ "$current_value" =~ ^[0-9]+$ && "$recommended_value" =~ ^[0-9]+$ ]]; then
        echo "→ 数値変更 ($current_value$recommended_value)"
    fi
}

これにより、

  • 数値が増えた/減った
  • true / false が切り替わった
  • URL が変更された

といった情報を、ログ上で直感的に把握できます。

使用方法

スクリプトの実行

cd dify/docker
chmod +x dify-env-sync.sh
./dify-env-sync.sh

特別なオプションは不要で、
そのまま実行するだけで動作します。

実行ログの例

実行時には、以下のようなログが出力されます。

  • バックアップ作成
  • 差分検出結果
  • 保持された設定 / 追加された設定

これにより、「何が起きたのか」を後から追いやすくなっています。

スクリプトの特徴

差分の可視化

検出された差分は、単なる一覧ではなく「意味のある変更」として表示されます。

種類
数値変更 UPLOAD_FILE_SIZE_LIMIT: 50 → 15
ブール値変更 DEBUG: true → false
URL変更 API_URL: localhost → api.example.com

これにより、「今回はどこを重点的に確認すべきか」が一目で分かります。

カスタム設定の保護

本番環境用に調整した設定は、スクリプト実行後も保持されます。

UPLOAD_FILE_SIZE_LIMIT=50
DB_HOST=prod-db-server

.env.example 側のデフォルト値がどうなっていても、
意図しない上書きは発生しません

新規変数の自動追加

一方で、新しいバージョンで追加された変数は自動で .env に追加されます。

WORKFLOW_MAX_EXECUTION_STEPS=500
MARKETPLACE_ENABLED=true

これにより、「知らないうちに必要な設定が足りない」という事態を防げます。

実運用での使いどころ

実際の運用では、次のような流れで使っています。

  1. .env.example の差分を確認
  2. スクリプトで同期
  3. DB・セキュリティ関連の設定を重点チェック
  4. Docker を再起動

ステージング環境で一度実行してから本番に適用することで、
かなり安心感を持ってバージョンアップできるようになりました。

まとめ

Dify のバージョンアップにおける環境変数管理は、
放置すると確実に運用コストが積み上がっていくポイントです。

今回紹介したスクリプトを使うことで、

  • 手動作業の大幅削減
  • 本番設定の安全な保護
  • 差分の見える化

を同時に実現できます。

「毎回 .env.example をにらみながら祈るように更新している」
そんな状況から抜け出したい方の参考になれば幸いです。

参考リンク

株式会社ZOZO

Discussion