T-CREATOR

Dify のデータベース設定と永続化ストレージの使い方

Dify のデータベース設定と永続化ストレージの使い方

AI アプリケーションの開発において、データの永続化と効率的な管理は成功の鍵を握る重要な要素です。Dify を本格的に活用するためには、適切なデータベース設定と永続化ストレージの理解が欠かせません。

多くの開発者が初期段階では簡単な設定で始めがちですが、アプリケーションの成長とともにデータ量が増大し、パフォーマンスやセキュリティの課題に直面することになります。本記事では、開発環境での基本設定から本番環境での本格運用まで、段階的にマスターできる実践的な手順をご紹介いたします。

実際のエラーコードや設定例も豊富に含めておりますので、すぐに実践していただけます。データベース設計の基礎から高度な最適化技術まで、包括的に解説いたしますので、ぜひ最後までお読みください。

Dify のデータベースアーキテクチャ理解

データベース構成の全体像

Dify のデータベースアーキテクチャは、異なる役割を持つ複数のデータベースが連携して動作する設計となっています。この多層構造により、高いパフォーマンスと拡張性を実現しています。

typescript// Dify データベース構成の概要
interface DifyDatabaseArchitecture {
  // メインデータベース(PostgreSQL)
  mainDatabase: {
    name: 'PostgreSQL';
    purpose: 'アプリケーションデータ、ユーザー情報、設定データ';
    port: 5432;
    persistence: true;
  };

  // キャッシュデータベース(Redis)
  cacheDatabase: {
    name: 'Redis';
    purpose: 'セッション管理、一時データ、高速キャッシュ';
    port: 6379;
    persistence: 'configurable';
  };

  // ベクターデータベース
  vectorDatabase: {
    name: 'Pinecone | Weaviate | Chroma';
    purpose: 'エンベディング、セマンティック検索';
    connection: 'API | Self-hosted';
    persistence: true;
  };

  // ファイルストレージ
  fileStorage: {
    name: 'LocalFS | S3 | MinIO';
    purpose: 'ファイル、画像、ドキュメント';
    persistence: true;
    backup: 'required';
  };
}

この構成により、各データベースが最適化された役割を担い、全体として高いパフォーマンスを実現します。

各データベースの役割と特徴

各データベースの詳細な役割と特徴を理解することで、適切な設定と運用が可能になります。

PostgreSQL の役割と特徴

#項目詳細重要度
1ユーザー管理アカウント情報、権限、プロファイル
2アプリ設定ワークフロー、プロンプト、設定値
3実行履歴ログ、実行結果、エラー情報
4課金情報使用量、料金、サブスクリプション
sql-- PostgreSQL テーブル構造の例
CREATE TABLE dify_apps (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    name VARCHAR(255) NOT NULL,
    description TEXT,
    mode VARCHAR(50) NOT NULL CHECK (mode IN ('chat', 'workflow')),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    tenant_id UUID NOT NULL REFERENCES tenants(id),
    INDEX idx_tenant_apps (tenant_id, created_at)
);

CREATE TABLE conversations (
    id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
    app_id UUID NOT NULL REFERENCES dify_apps(id),
    user_id UUID,
    status VARCHAR(20) DEFAULT 'active',
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    INDEX idx_app_conversations (app_id, created_at),
    INDEX idx_user_conversations (user_id, created_at)
);

Redis の役割と特徴

Redis は高速なインメモリデータベースとして、以下の用途で活用されます:

typescript// Redis データ構造の設計例
interface RedisDataStructure {
  // セッション管理
  sessions: {
    key: `session:${userId}:${sessionId}`;
    value: {
      userId: string;
      appId: string;
      lastActivity: number;
      preferences: object;
    };
    ttl: 86400; // 24時間
  };

  // API レート制限
  rateLimit: {
    key: `rate_limit:${apiKey}:${endpoint}`;
    value: number; // リクエスト数
    ttl: 3600; // 1時間
  };

  // 一時的なワークフロー状態
  workflowState: {
    key: `workflow:${workflowId}:state`;
    value: {
      currentStep: number;
      variables: object;
      status: 'running' | 'paused' | 'completed';
    };
    ttl: 3600;
  };
}

データフローの仕組み

Dify におけるデータフローは、リクエストの種類に応じて最適化されたパスを通ります。

typescript// データフロー処理の実装例
class DifyDataFlow {
  async processUserRequest(
    request: UserRequest
  ): Promise<Response> {
    try {
      // 1. Redis でセッション確認
      const session = await this.redis.get(
        `session:${request.userId}`
      );
      if (!session) {
        throw new Error('DIFY_SESSION_EXPIRED');
      }

      // 2. PostgreSQL でユーザー権限確認
      const user = await this.postgres.query(
        'SELECT * FROM users WHERE id = $1 AND status = $2',
        [request.userId, 'active']
      );

      if (!user.rows.length) {
        throw new Error('DIFY_USER_NOT_FOUND');
      }

      // 3. ベクターデータベースで関連情報検索
      const embeddings = await this.vectorDB.search({
        query: request.query,
        topK: 10,
        filter: { userId: request.userId },
      });

      // 4. 結果をキャッシュに保存
      await this.redis.setex(
        `cache:${request.requestId}`,
        300, // 5分
        JSON.stringify(embeddings)
      );

      return { success: true, data: embeddings };
    } catch (error) {
      console.error('Data flow error:', error.message);
      throw error;
    }
  }
}

このデータフローにより、効率的で信頼性の高いデータ処理が実現されます。

開発環境でのデータベース設定

Docker Compose を使った基本設定

開発環境では Docker Compose を使用することで、複数のデータベースを簡単に管理できます。以下は実際の設定例です。

yaml# docker-compose.yml
version: '3.8'

services:
  # PostgreSQL メインデータベース
  postgres:
    image: postgres:15-alpine
    container_name: dify-postgres
    environment:
      POSTGRES_DB: dify
      POSTGRES_USER: dify
      POSTGRES_PASSWORD: dify123!
      POSTGRES_INITDB_ARGS: '--encoding=UTF-8 --locale=C'
    ports:
      - '5432:5432'
    volumes:
      - postgres_data:/var/lib/postgresql/data
      - ./init-scripts:/docker-entrypoint-initdb.d
    healthcheck:
      test: ['CMD-SHELL', 'pg_isready -U dify -d dify']
      interval: 30s
      timeout: 10s
      retries: 3
    networks:
      - dify-network

  # Redis キャッシュ
  redis:
    image: redis:7-alpine
    container_name: dify-redis
    command: redis-server --appendonly yes --requirepass redis123!
    ports:
      - '6379:6379'
    volumes:
      - redis_data:/data
      - ./redis.conf:/usr/local/etc/redis/redis.conf
    healthcheck:
      test: ['CMD', 'redis-cli', '--raw', 'incr', 'ping']
      interval: 30s
      timeout: 10s
      retries: 3
    networks:
      - dify-network

  # Weaviate ベクターデータベース
  weaviate:
    image: semitechnologies/weaviate:1.21.2
    container_name: dify-weaviate
    environment:
      QUERY_DEFAULTS_LIMIT: 25
      AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'false'
      AUTHENTICATION_APIKEY_ENABLED: 'true'
      AUTHENTICATION_APIKEY_ALLOWED_KEYS: 'weaviate-api-key'
      AUTHENTICATION_APIKEY_USERS: 'admin'
      PERSISTENCE_DATA_PATH: '/var/lib/weaviate'
      DEFAULT_VECTORIZER_MODULE: 'none'
      ENABLE_MODULES: 'text2vec-openai,generative-openai'
      CLUSTER_HOSTNAME: 'node1'
    ports:
      - '8080:8080'
    volumes:
      - weaviate_data:/var/lib/weaviate
    networks:
      - dify-network

volumes:
  postgres_data:
  redis_data:
  weaviate_data:

networks:
  dify-network:
    driver: bridge

環境変数の設定方法

適切な環境変数設定により、セキュリティと柔軟性を両立できます。

bash# .env.development
# データベース接続設定
DB_USERNAME=dify
DB_PASSWORD=dify123!
DB_HOST=localhost
DB_PORT=5432
DB_DATABASE=dify

# Redis 設定
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=redis123!
REDIS_DB=0

# ベクターデータベース設定
VECTOR_STORE=weaviate
WEAVIATE_ENDPOINT=http://localhost:8080
WEAVIATE_API_KEY=weaviate-api-key

# ファイルストレージ設定
STORAGE_TYPE=local
STORAGE_LOCAL_PATH=./storage

# セキュリティ設定
SECRET_KEY=your-secret-key-here
ENCRYPT_KEY=your-encrypt-key-here

# ログレベル
LOG_LEVEL=DEBUG
typescript// 環境変数の型安全な読み込み
interface DatabaseConfig {
  postgres: {
    host: string;
    port: number;
    database: string;
    username: string;
    password: string;
    ssl: boolean;
  };
  redis: {
    host: string;
    port: number;
    password: string;
    db: number;
  };
  vector: {
    provider: 'weaviate' | 'pinecone' | 'chroma';
    endpoint: string;
    apiKey: string;
  };
}

function loadDatabaseConfig(): DatabaseConfig {
  const requiredEnvVars = [
    'DB_HOST',
    'DB_PORT',
    'DB_DATABASE',
    'DB_USERNAME',
    'DB_PASSWORD',
    'REDIS_HOST',
    'REDIS_PORT',
  ];

  for (const envVar of requiredEnvVars) {
    if (!process.env[envVar]) {
      throw new Error(
        `DIFY_CONFIG_MISSING: ${envVar} is required`
      );
    }
  }

  return {
    postgres: {
      host: process.env.DB_HOST!,
      port: parseInt(process.env.DB_PORT!),
      database: process.env.DB_DATABASE!,
      username: process.env.DB_USERNAME!,
      password: process.env.DB_PASSWORD!,
      ssl: process.env.DB_SSL === 'true',
    },
    redis: {
      host: process.env.REDIS_HOST!,
      port: parseInt(process.env.REDIS_PORT!),
      password: process.env.REDIS_PASSWORD || '',
      db: parseInt(process.env.REDIS_DB || '0'),
    },
    vector: {
      provider:
        (process.env.VECTOR_STORE as any) || 'weaviate',
      endpoint: process.env.WEAVIATE_ENDPOINT!,
      apiKey: process.env.WEAVIATE_API_KEY!,
    },
  };
}

初期データベース構築手順

データベースの初期構築では、適切な順序で設定を行うことが重要です。

sql-- init-scripts/01-create-database.sql
-- データベースとユーザーの作成
CREATE DATABASE dify_dev;
CREATE USER dify_user WITH ENCRYPTED PASSWORD 'dify123!';
GRANT ALL PRIVILEGES ON DATABASE dify_dev TO dify_user;

-- 拡張機能の有効化
\c dify_dev;
CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
CREATE EXTENSION IF NOT EXISTS "pg_trgm";
CREATE EXTENSION IF NOT EXISTS "btree_gin";
sql-- init-scripts/02-create-tables.sql
-- テナントテーブル
CREATE TABLE tenants (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    name VARCHAR(255) NOT NULL,
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- ユーザーテーブル
CREATE TABLE users (
    id UUID PRIMARY KEY DEFAULT uuid_generate_v4(),
    tenant_id UUID NOT NULL REFERENCES tenants(id),
    email VARCHAR(255) UNIQUE NOT NULL,
    password_hash VARCHAR(255) NOT NULL,
    name VARCHAR(255) NOT NULL,
    status VARCHAR(20) DEFAULT 'active' CHECK (status IN ('active', 'inactive', 'suspended')),
    created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
    updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
);

-- インデックスの作成
CREATE INDEX idx_users_tenant ON users(tenant_id);
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_users_status ON users(status);
typescript// データベース初期化スクリプト
class DatabaseInitializer {
  constructor(private config: DatabaseConfig) {}

  async initialize(): Promise<void> {
    try {
      // PostgreSQL 接続テスト
      await this.testPostgreSQLConnection();

      // Redis 接続テスト
      await this.testRedisConnection();

      // ベクターデータベース接続テスト
      await this.testVectorDatabaseConnection();

      // 初期データの投入
      await this.seedInitialData();

      console.log(
        'Database initialization completed successfully'
      );
    } catch (error) {
      console.error(
        'Database initialization failed:',
        error
      );
      throw error;
    }
  }

  private async testPostgreSQLConnection(): Promise<void> {
    const { Client } = require('pg');
    const client = new Client(this.config.postgres);

    try {
      await client.connect();
      const result = await client.query('SELECT version()');
      console.log(
        'PostgreSQL connected:',
        result.rows[0].version
      );
    } catch (error) {
      if (error.code === 'ECONNREFUSED') {
        throw new Error(
          'DIFY_POSTGRES_CONNECTION_REFUSED: PostgreSQL server is not running'
        );
      } else if (error.code === '28P01') {
        throw new Error(
          'DIFY_POSTGRES_AUTH_FAILED: Invalid username or password'
        );
      } else if (error.code === '3D000') {
        throw new Error(
          'DIFY_POSTGRES_DB_NOT_FOUND: Database does not exist'
        );
      }
      throw error;
    } finally {
      await client.end();
    }
  }

  private async testRedisConnection(): Promise<void> {
    const Redis = require('ioredis');
    const redis = new Redis(this.config.redis);

    try {
      await redis.ping();
      console.log('Redis connected successfully');
    } catch (error) {
      if (error.code === 'ECONNREFUSED') {
        throw new Error(
          'DIFY_REDIS_CONNECTION_REFUSED: Redis server is not running'
        );
      } else if (error.message.includes('WRONGPASS')) {
        throw new Error(
          'DIFY_REDIS_AUTH_FAILED: Invalid Redis password'
        );
      }
      throw error;
    } finally {
      redis.disconnect();
    }
  }

  private async testVectorDatabaseConnection(): Promise<void> {
    // Weaviate 接続テストの例
    const response = await fetch(
      `${this.config.vector.endpoint}/v1/meta`,
      {
        headers: {
          Authorization: `Bearer ${this.config.vector.apiKey}`,
        },
      }
    );

    if (!response.ok) {
      if (response.status === 401) {
        throw new Error(
          'DIFY_VECTOR_AUTH_FAILED: Invalid API key'
        );
      } else if (response.status === 404) {
        throw new Error(
          'DIFY_VECTOR_ENDPOINT_NOT_FOUND: Invalid endpoint URL'
        );
      }
      throw new Error(
        `DIFY_VECTOR_CONNECTION_FAILED: ${response.statusText}`
      );
    }

         console.log('Vector database connected successfully');
   }
 }

# 本番環境でのデータベース構築

## PostgreSQL の本格設定

本番環境では、パフォーマンス、セキュリティ、可用性を重視した設定が必要です。

````sql
-- postgresql.conf の重要な設定項目
# メモリ設定
shared_buffers = 256MB                    # 利用可能メモリの25%
effective_cache_size = 1GB                # システムキャッシュサイズ
work_mem = 4MB                           # ソート・ハッシュ用メモリ
maintenance_work_mem = 64MB              # メンテナンス用メモリ

# 接続設定
max_connections = 100                     # 最大同時接続数
listen_addresses = '*'                    # 接続許可アドレス

# WAL設定(Write-Ahead Logging)
wal_level = replica                       # レプリケーション対応
max_wal_size = 1GB                       # WAL最大サイズ
min_wal_size = 80MB                      # WAL最小サイズ
checkpoint_completion_target = 0.7        # チェックポイント完了目標

# ログ設定
logging_collector = on                    # ログ収集有効
log_directory = 'pg_log'                 # ログディレクトリ
log_filename = 'postgresql-%Y-%m-%d_%H%M%S.log'
log_min_duration_statement = 1000        # 1秒以上のクエリをログ出力
log_checkpoints = on                     # チェックポイントログ
log_connections = on                     # 接続ログ
log_disconnections = on                  # 切断ログ
typescript// 本番環境用 PostgreSQL 接続プール設定
import { Pool } from 'pg';

class ProductionPostgreSQLManager {
  private pool: Pool;

  constructor() {
    this.pool = new Pool({
      host: process.env.DB_HOST,
      port: parseInt(process.env.DB_PORT || '5432'),
      database: process.env.DB_DATABASE,
      user: process.env.DB_USERNAME,
      password: process.env.DB_PASSWORD,

      // 接続プール設定
      min: 10, // 最小接続数
      max: 30, // 最大接続数
      idleTimeoutMillis: 30000, // アイドルタイムアウト
      connectionTimeoutMillis: 2000, // 接続タイムアウト

      // SSL設定
      ssl: {
        rejectUnauthorized: false,
        ca: process.env.DB_SSL_CA,
        key: process.env.DB_SSL_KEY,
        cert: process.env.DB_SSL_CERT,
      },

      // エラーハンドリング
      query_timeout: 30000, // クエリタイムアウト
      statement_timeout: 30000, // ステートメントタイムアウト
    });

    this.setupErrorHandling();
  }

  private setupErrorHandling(): void {
    this.pool.on('error', (err) => {
      console.error('PostgreSQL pool error:', err);

      // 具体的なエラーコードに応じた処理
      switch (err.code) {
        case 'ECONNRESET':
          console.error(
            'DIFY_POSTGRES_CONNECTION_RESET: Connection was reset'
          );
          break;
        case 'ENOTFOUND':
          console.error(
            'DIFY_POSTGRES_HOST_NOT_FOUND: Database host not found'
          );
          break;
        case '57P01':
          console.error(
            'DIFY_POSTGRES_ADMIN_SHUTDOWN: Database is shutting down'
          );
          break;
        case '53300':
          console.error(
            'DIFY_POSTGRES_TOO_MANY_CONNECTIONS: Too many connections'
          );
          break;
        default:
          console.error(
            'DIFY_POSTGRES_UNKNOWN_ERROR:',
            err.message
          );
      }
    });
  }

  async executeQuery(
    query: string,
    params?: any[]
  ): Promise<any> {
    const client = await this.pool.connect();
    try {
      const result = await client.query(query, params);
      return result;
    } catch (error) {
      console.error('Query execution error:', error);
      throw error;
    } finally {
      client.release();
    }
  }
}

Redis の最適化設定

本番環境での Redis は、メモリ効率とパフォーマンスの最適化が重要です。

conf# redis.conf 本番環境設定
# メモリ設定
maxmemory 2gb                            # 最大メモリ使用量
maxmemory-policy allkeys-lru             # メモリ不足時の削除ポリシー

# 永続化設定
save 900 1                               # 900秒で1回以上の変更があった場合に保存
save 300 10                              # 300秒で10回以上の変更があった場合に保存
save 60 10000                            # 60秒で10000回以上の変更があった場合に保存

# AOF(Append Only File)設定
appendonly yes                           # AOF有効化
appendfilename "appendonly.aof"          # AOFファイル名
appendfsync everysec                     # 1秒ごとに同期

# セキュリティ設定
requirepass your-strong-password         # パスワード認証
rename-command FLUSHDB ""                # 危険なコマンドを無効化
rename-command FLUSHALL ""
rename-command DEBUG ""

# ネットワーク設定
bind 127.0.0.1 10.0.0.1                # バインドアドレス
port 6379                               # ポート番号
timeout 300                             # クライアントタイムアウト

# ログ設定
loglevel notice                         # ログレベル
logfile /var/log/redis/redis-server.log # ログファイル
typescript// Redis クラスター設定(高可用性対応)
import Redis from 'ioredis';

class ProductionRedisManager {
  private redis: Redis.Cluster;
  private subscriber: Redis.Cluster;

  constructor() {
    const clusterNodes = [
      { host: process.env.REDIS_NODE1_HOST, port: 6379 },
      { host: process.env.REDIS_NODE2_HOST, port: 6379 },
      { host: process.env.REDIS_NODE3_HOST, port: 6379 },
    ];

    const clusterOptions = {
      password: process.env.REDIS_PASSWORD,
      redisOptions: {
        connectTimeout: 10000,
        lazyConnect: true,
        maxRetriesPerRequest: 3,
        retryDelayOnFailover: 100,
        enableOfflineQueue: false,
      },
      clusterRetryDelayOnFailover: 100,
      clusterRetryDelayOnClusterDown: 300,
      maxRetriesPerRequest: 3,
    };

    this.redis = new Redis.Cluster(
      clusterNodes,
      clusterOptions
    );
    this.subscriber = new Redis.Cluster(
      clusterNodes,
      clusterOptions
    );

    this.setupErrorHandling();
  }

  private setupErrorHandling(): void {
    this.redis.on('error', (error) => {
      console.error('Redis cluster error:', error);

      if (error.message.includes('CLUSTERDOWN')) {
        console.error(
          'DIFY_REDIS_CLUSTER_DOWN: Redis cluster is down'
        );
      } else if (error.message.includes('MOVED')) {
        console.error(
          'DIFY_REDIS_SLOT_MOVED: Hash slot moved to different node'
        );
      } else if (error.message.includes('NOAUTH')) {
        console.error(
          'DIFY_REDIS_AUTH_FAILED: Authentication failed'
        );
      }
    });

    this.redis.on('ready', () => {
      console.log('Redis cluster ready');
    });
  }

  async setWithExpiry(
    key: string,
    value: any,
    ttl: number
  ): Promise<void> {
    try {
      await this.redis.setex(
        key,
        ttl,
        JSON.stringify(value)
      );
    } catch (error) {
      console.error('Redis set error:', error);
      throw new Error(
        `DIFY_REDIS_SET_FAILED: ${error.message}`
      );
    }
  }

  async get(key: string): Promise<any> {
    try {
      const value = await this.redis.get(key);
      return value ? JSON.parse(value) : null;
    } catch (error) {
      console.error('Redis get error:', error);
      throw new Error(
        `DIFY_REDIS_GET_FAILED: ${error.message}`
      );
    }
  }
}

Vector データベースの選択と設定

ベクターデータベースの選択は、用途と規模に応じて決定します。

Pinecone の設定(マネージドサービス)

typescript// Pinecone 設定例
import { PineconeClient } from '@pinecone-database/pinecone';

class PineconeVectorStore {
  private pinecone: PineconeClient;
  private indexName: string;

  constructor() {
    this.pinecone = new PineconeClient();
    this.indexName =
      process.env.PINECONE_INDEX_NAME || 'dify-embeddings';
  }

  async initialize(): Promise<void> {
    try {
      await this.pinecone.init({
        environment: process.env.PINECONE_ENVIRONMENT!,
        apiKey: process.env.PINECONE_API_KEY!,
      });

      // インデックスの存在確認
      const indexList = await this.pinecone.listIndexes();
      if (!indexList.includes(this.indexName)) {
        await this.createIndex();
      }
    } catch (error) {
      if (error.message.includes('401')) {
        throw new Error(
          'DIFY_PINECONE_AUTH_FAILED: Invalid API key'
        );
      } else if (error.message.includes('403')) {
        throw new Error(
          'DIFY_PINECONE_QUOTA_EXCEEDED: API quota exceeded'
        );
      }
      throw error;
    }
  }

  private async createIndex(): Promise<void> {
    await this.pinecone.createIndex({
      createRequest: {
        name: this.indexName,
        dimension: 1536, // OpenAI embeddings dimension
        metric: 'cosine',
        pods: 1,
        replicas: 1,
        pod_type: 'p1.x1',
      },
    });
  }

  async upsertVectors(
    vectors: Array<{
      id: string;
      values: number[];
      metadata?: object;
    }>
  ): Promise<void> {
    try {
      const index = this.pinecone.Index(this.indexName);
      await index.upsert({
        upsertRequest: {
          vectors: vectors,
        },
      });
    } catch (error) {
      console.error('Pinecone upsert error:', error);
      throw new Error(
        `DIFY_PINECONE_UPSERT_FAILED: ${error.message}`
      );
    }
  }
}

Weaviate の自己ホスト設定

yaml# docker-compose.prod.yml (Weaviate部分)
weaviate:
  image: semitechnologies/weaviate:1.21.2
  container_name: dify-weaviate-prod
  restart: always
  environment:
    QUERY_DEFAULTS_LIMIT: 25
    AUTHENTICATION_ANONYMOUS_ACCESS_ENABLED: 'false'
    AUTHENTICATION_APIKEY_ENABLED: 'true'
    AUTHENTICATION_APIKEY_ALLOWED_KEYS: '${WEAVIATE_API_KEY}'
    AUTHENTICATION_APIKEY_USERS: 'admin'
    PERSISTENCE_DATA_PATH: '/var/lib/weaviate'
    DEFAULT_VECTORIZER_MODULE: 'none'
    ENABLE_MODULES: 'text2vec-openai,generative-openai,backup-filesystem'
    CLUSTER_HOSTNAME: 'node1'
    BACKUP_FILESYSTEM_PATH: '/var/lib/weaviate-backups'
  ports:
    - '8080:8080'
  volumes:
    - weaviate_data:/var/lib/weaviate
    - weaviate_backups:/var/lib/weaviate-backups
  healthcheck:
    test:
      ['CMD', 'curl', '-f', 'http://localhost:8080/v1/meta']
    interval: 30s
    timeout: 10s
    retries: 3
  deploy:
    resources:
      limits:
        memory: 4G
        cpus: '2'
      reservations:
        memory: 2G
        cpus: '1'

永続化ストレージの実装

ファイルストレージの設定

ファイルストレージは、アップロードされたドキュメントや生成されたファイルを保存します。

typescript// ファイルストレージインターface
interface FileStorageProvider {
  upload(file: Buffer, path: string): Promise<string>;
  download(path: string): Promise<Buffer>;
  delete(path: string): Promise<void>;
  exists(path: string): Promise<boolean>;
  getUrl(path: string): Promise<string>;
}

// ローカルファイルストレージ実装
class LocalFileStorage implements FileStorageProvider {
  private basePath: string;

  constructor(basePath: string = './storage') {
    this.basePath = basePath;
    this.ensureDirectory();
  }

  private ensureDirectory(): void {
    const fs = require('fs');
    if (!fs.existsSync(this.basePath)) {
      fs.mkdirSync(this.basePath, { recursive: true });
    }
  }

  async upload(
    file: Buffer,
    path: string
  ): Promise<string> {
    const fs = require('fs').promises;
    const fullPath = `${this.basePath}/${path}`;
    const directory = fullPath.substring(
      0,
      fullPath.lastIndexOf('/')
    );

    try {
      await fs.mkdir(directory, { recursive: true });
      await fs.writeFile(fullPath, file);
      return fullPath;
    } catch (error) {
      if (error.code === 'ENOSPC') {
        throw new Error(
          'DIFY_STORAGE_NO_SPACE: Insufficient disk space'
        );
      } else if (error.code === 'EACCES') {
        throw new Error(
          'DIFY_STORAGE_PERMISSION_DENIED: Permission denied'
        );
      }
      throw new Error(
        `DIFY_STORAGE_UPLOAD_FAILED: ${error.message}`
      );
    }
  }

  async download(path: string): Promise<Buffer> {
    const fs = require('fs').promises;
    const fullPath = `${this.basePath}/${path}`;

    try {
      return await fs.readFile(fullPath);
    } catch (error) {
      if (error.code === 'ENOENT') {
        throw new Error(
          'DIFY_STORAGE_FILE_NOT_FOUND: File not found'
        );
      }
      throw new Error(
        `DIFY_STORAGE_DOWNLOAD_FAILED: ${error.message}`
      );
    }
  }

  async exists(path: string): Promise<boolean> {
    const fs = require('fs').promises;
    const fullPath = `${this.basePath}/${path}`;

    try {
      await fs.access(fullPath);
      return true;
    } catch {
      return false;
    }
  }

  async delete(path: string): Promise<void> {
    const fs = require('fs').promises;
    const fullPath = `${this.basePath}/${path}`;

    try {
      await fs.unlink(fullPath);
    } catch (error) {
      if (error.code === 'ENOENT') {
        throw new Error(
          'DIFY_STORAGE_FILE_NOT_FOUND: File not found'
        );
      }
      throw new Error(
        `DIFY_STORAGE_DELETE_FAILED: ${error.message}`
      );
    }
  }

  async getUrl(path: string): Promise<string> {
    const baseUrl =
      process.env.BASE_URL || 'http://localhost:3000';
    return `${baseUrl}/files/${path}`;
  }
}

オブジェクトストレージとの連携

本番環境では、AWS S3 や MinIO などのオブジェクトストレージの使用を推奨します。

typescript// AWS S3 ストレージ実装
import AWS from 'aws-sdk';

class S3FileStorage implements FileStorageProvider {
  private s3: AWS.S3;
  private bucketName: string;

  constructor() {
    this.s3 = new AWS.S3({
      accessKeyId: process.env.AWS_ACCESS_KEY_ID,
      secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY,
      region: process.env.AWS_REGION || 'us-east-1',
    });
    this.bucketName = process.env.S3_BUCKET_NAME!;
  }

  async upload(
    file: Buffer,
    path: string
  ): Promise<string> {
    try {
      const params = {
        Bucket: this.bucketName,
        Key: path,
        Body: file,
        ContentType: this.getContentType(path),
        ServerSideEncryption: 'AES256',
      };

      const result = await this.s3.upload(params).promise();
      return result.Location;
    } catch (error) {
      if (error.code === 'NoSuchBucket') {
        throw new Error(
          'DIFY_S3_BUCKET_NOT_FOUND: S3 bucket does not exist'
        );
      } else if (error.code === 'AccessDenied') {
        throw new Error(
          'DIFY_S3_ACCESS_DENIED: Insufficient S3 permissions'
        );
      } else if (error.code === 'InvalidAccessKeyId') {
        throw new Error(
          'DIFY_S3_INVALID_CREDENTIALS: Invalid AWS credentials'
        );
      }
      throw new Error(
        `DIFY_S3_UPLOAD_FAILED: ${error.message}`
      );
    }
  }

  private getContentType(path: string): string {
    const extension = path.split('.').pop()?.toLowerCase();
    const contentTypes: { [key: string]: string } = {
      pdf: 'application/pdf',
      txt: 'text/plain',
      docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
      xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
      png: 'image/png',
      jpg: 'image/jpeg',
      jpeg: 'image/jpeg',
    };
    return (
      contentTypes[extension || ''] ||
      'application/octet-stream'
    );
  }
}

データバックアップ戦略

包括的なバックアップ戦略により、データ損失のリスクを最小限に抑えます。

バックアップスケジュールの設計

#バックアップ種類頻度保存期間対象データ
1完全バックアップ週 1 回3 ヶ月全データベース・ファイル
2増分バックアップ日 1 回2 週間変更されたデータのみ
3差分バックアップ時間 1 回3 日間前回完全バックアップ以降
4設定バックアップ変更時1 年間設定ファイル・環境変数
typescript// バックアップ管理システム
class BackupManager {
  private config: {
    postgres: DatabaseConfig['postgres'];
    redis: DatabaseConfig['redis'];
    storage: FileStorageProvider;
  };

  constructor(config: any) {
    this.config = config;
  }

  async performFullBackup(): Promise<void> {
    const backupId = `backup_${Date.now()}`;
    console.log(`Starting full backup: ${backupId}`);

    try {
      // PostgreSQL バックアップ
      await this.backupPostgreSQL(backupId);

      // Redis バックアップ
      await this.backupRedis(backupId);

      // ファイルストレージバックアップ
      await this.backupFileStorage(backupId);

      // バックアップメタデータの保存
      await this.saveBackupMetadata(backupId);

      console.log(`Full backup completed: ${backupId}`);
    } catch (error) {
      console.error(`Backup failed: ${error.message}`);
      throw error;
    }
  }

  private async backupPostgreSQL(
    backupId: string
  ): Promise<void> {
    const { spawn } = require('child_process');
    const fs = require('fs');

    return new Promise((resolve, reject) => {
      const backupFile = `./backups/${backupId}_postgres.sql`;
      const writeStream = fs.createWriteStream(backupFile);

      const pgDump = spawn(
        'pg_dump',
        [
          '-h',
          this.config.postgres.host,
          '-p',
          this.config.postgres.port.toString(),
          '-U',
          this.config.postgres.username,
          '-d',
          this.config.postgres.database,
          '--no-password',
          '--verbose',
          '--clean',
          '--if-exists',
        ],
        {
          env: {
            ...process.env,
            PGPASSWORD: this.config.postgres.password,
          },
        }
      );

      pgDump.stdout.pipe(writeStream);

      pgDump.on('error', (error) => {
        if (error.code === 'ENOENT') {
          reject(
            new Error(
              'DIFY_BACKUP_PGDUMP_NOT_FOUND: pg_dump command not found'
            )
          );
        } else {
          reject(
            new Error(
              `DIFY_BACKUP_POSTGRES_FAILED: ${error.message}`
            )
          );
        }
      });

      pgDump.on('close', (code) => {
        if (code === 0) {
          resolve();
        } else {
          reject(
            new Error(
              `DIFY_BACKUP_POSTGRES_EXIT_CODE: ${code}`
            )
          );
        }
      });
    });
  }

  private async backupRedis(
    backupId: string
  ): Promise<void> {
    const Redis = require('ioredis');
    const redis = new Redis(this.config.redis);

    try {
      // Redis BGSAVE コマンドでバックアップ作成
      await redis.bgsave();

      // バックアップ完了まで待機
      let isCompleted = false;
      while (!isCompleted) {
        const lastSave = await redis.lastsave();
        await new Promise((resolve) =>
          setTimeout(resolve, 1000)
        );
        const currentSave = await redis.lastsave();
        isCompleted = currentSave > lastSave;
      }

      console.log(`Redis backup completed: ${backupId}`);
    } catch (error) {
      throw new Error(
        `DIFY_BACKUP_REDIS_FAILED: ${error.message}`
      );
    } finally {
      redis.disconnect();
    }
  }

  private async saveBackupMetadata(
    backupId: string
  ): Promise<void> {
    const metadata = {
      backupId,
      timestamp: new Date().toISOString(),
      components: ['postgresql', 'redis', 'file_storage'],
      size: await this.calculateBackupSize(backupId),
      status: 'completed',
    };

    await this.config.storage.upload(
      Buffer.from(JSON.stringify(metadata, null, 2)),
      `backups/${backupId}/metadata.json`
    );
  }
}

まとめ

本記事では、Dify のデータベース設定と永続化ストレージについて、開発環境から本番環境まで段階的に解説いたしました。

適切なデータベース設計と永続化戦略は、AI アプリケーションの成功に不可欠な要素です。PostgreSQL、Redis、ベクターデータベースそれぞれの特性を理解し、用途に応じて最適化することで、高いパフォーマンスと信頼性を実現できます。

特に重要なポイントとして、以下の点を再度強調いたします:

データベース設計の原則

  • 各データベースの役割を明確に分離し、適切な用途で使用する
  • 接続プールやキャッシュ戦略により、パフォーマンスを最適化する
  • エラーハンドリングを充実させ、運用時のトラブルに備える

セキュリティとバックアップ

  • 本番環境では必ず暗号化と認証を実装する
  • 定期的なバックアップと復旧テストを実施する
  • 監査ログを活用し、セキュリティインシデントに備える

運用とメンテナンス

  • 監視システムを構築し、問題を早期発見する
  • 定期的なメンテナンスでパフォーマンスを維持する
  • 容量計画を立て、スケーラビリティに対応する

これらの知識を活用して、堅牢で高性能な Dify 環境を構築し、AI アプリケーション開発を成功に導いてください。データベースとストレージの適切な管理により、ユーザーに価値ある AI 体験を提供できるでしょう。

関連リンク