🌀

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

に公開
2025/12/24

はじめに

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