メインマシンをリプレイスした際、手元に残った「メモリ8GB / 256GB SSD」のM1 Mac mini。
これを「24時間稼働のローカル・エッジサーバー」として再構築し、n8n、Dify、Ollama(ローカルLLM)を稼働させるアーキテクチャを紹介します。
結論として、8GBメモリでこれらのスタックをそのまま同時稼働させることは不可能です。
本記事は、物理的なハードウェアの限界内で折り合いをつけ、OSのチューニングとアーキテクチャの分離によってシステムを安定稼働させるための設計戦略です。
許容すべき「4つのトレードオフ」
この構成を採用するにあたり、以下のトレードオフを前提とします。
- UXの低下(推論の都度ロード): メモリ枯渇を防ぐため、LLMを常駐させません。推論のたびにモデルをメモリにロードするため、レスレスポンスに数秒の遅延が発生します。
- SSDの寿命(TBW)リスク: 極限までチューニングしてもスワップの発生は避けられません。256GB SSDの寿命を縮めるリスクがあります。
- 用途の限定: 8GB環境で動かせるLLMは3B(約2GB)クラスが限界です。ローカルLLMは「単純なテキスト整形や分類」のコスト削減に特化させます。
-
Difyバージョンの固定(v0.15.x): Dify 1.0以降は
plugin_daemonやsandbox等の新規マイクロサービスが必須となり、8GBのメモリ予算を物理的に超過します。本記事では要件を満たす軽量で安定した最終バージョンである0.15.3を採用します。
メモリ予算(8GBの配分)
8GB(約8192MB)のメモリ配分設計です。
| コンポーネント | 制限 (Limits) | 役割・備考 |
|---|---|---|
| macOS (最適化後) | ~2000 MB | OSプロセス。UIアニメーション等は停止。 |
| OrbStack VM | ~500-800 MB | Docker Desktopの代替。負荷により変動。 |
| PostgreSQL 16 | 384 MB | n8n / Dify / pgvector 兼用。 |
| Redis 7 | 96 MB | Difyのキャッシュ・Celeryブローカー用。 |
| n8n | 512 MB | 自動化ハブ。 |
| Dify API (0.15.3) | 768 MB | バックエンドコア。ワーカー数を制限してOOMを防止。 |
| Dify Worker (0.15.3) | 512 MB | 非同期処理ワーカー。 |
| Dify Web (0.15.3) | 256 MB | フロントエンド。 |
| 常時稼働計 | 約 5.0-5.3 GB | 24時間稼働する基本スタック。 |
| Ollama (Qwen2.5 3B) | ~1.9 GB | n8nからAPI経由で推論時のみロードされる。 |
| ピーク時合計 | 約 6.9-7.2 GB | 余剰約0.8〜1.1GBをスワップ防止バッファとして確保。 |
Step 1: OrbStackへの移行とメモリ制限
Docker DesktopのVM維持オーバーヘッドを回避するため、OrbStack に完全移行します。
CLIコマンドを使用して、VMが使用できるリソース上限を明示的に制限します。
# OrbStackのVMメモリ上限を6GB(6144MiB)、CPUを4コアに設定
orb config set memory_mib 6144
orb config set cpu 4
# 設定を反映するためにOrbStackを再起動
orb stop && orb start
# ※設定が適用されたかは、OrbStackアプリの GUI (Settings > Resources) で確認してください。
# 後日GUIで設定を変更するとCLIで設定した値が上書きされるため注意が必要です。
Step 2: 前提イメージの確認とPull
OrbStackが起動し docker コマンドが使える状態になったら、構築を進める前に本記事で前提とするDockerイメージを取得します。フローティングタグによる予期せぬ破壊的アップデートを防ぐため、バージョンをピン留めしています。
docker pull pgvector/pgvector:0.8.0-pg16
docker pull n8nio/n8n:1.80.0
docker pull langgenius/dify-api:0.15.3
docker pull langgenius/dify-web:0.15.3
※もしタグが存在せず not found エラーになる場合は、Docker Hubで最新の安定版タグを確認し、記事内のバージョンを適宜読み替えてください。
Step 3: macOSのヘッドレス化
macOSの描画やインデックス作成のプロセスを停止し、リソースを確保します。
# スリープの完全無効化
sudo pmset -a sleep 0 disksleep 0 displaysleep 0 powernap 0
# 外部からSSHでウェイクできるようWake on LANを有効化(※主に有線LAN接続時に有効)
sudo pmset -a womp 1
# UIアニメーションと透明度の無効化
defaults write com.apple.universalaccess reduceMotion -bool true
defaults write NSGlobalDomain NSAutomaticWindowAnimationsEnabled -bool false
defaults write com.apple.universalaccess reduceTransparency -bool true
# Spotlightの自動インデックス無効化
sudo mdutil -a -i off
Step 4: Ollamaのネイティブ稼働と自動起動
OrbStackのLinux VMはGPUパススルーに対応していないため、Docker内でOllamaを動かすとCPU推論となり極端に遅延します。OllamaはmacOSネイティブアプリとして導入し、コンテナからは host.docker.internal 経由でアクセスさせます。
⚠️ セキュリティ注意
OLLAMA_HOST=0.0.0.0の設定は、同一LAN上の全デバイスからOllama APIへの無認証アクセスを許可します。信頼できるプライベートネットワーク(自宅LAN等)以外での使用は推奨しません。
【重要】brew services の回避
OllamaのHomebrewサービス(brew services start ollama)は、launchctl setenv 等で設定した環境変数を無視して 127.0.0.1 にバインドする既知の仕様・バグがあります。再起動後も確実にLANから接続させ、かつ推論後のメモリを自動解放(OLLAMA_KEEP_ALIVE=0)させるため、専用のカスタムLaunchAgentを作成します。
brew install ollama
1. com.local.ollama.plist の作成
YOUR_USERNAME の部分をご自身のMacのユーザー名に書き換えてください。
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.local.ollama</string>
<key>ProgramArguments</key>
<array>
<string>/opt/homebrew/bin/ollama</string>
<string>serve</string>
</array>
<key>EnvironmentVariables</key>
<dict>
<key>OLLAMA_HOST</key>
<string>0.0.0.0</string>
<key>OLLAMA_KEEP_ALIVE</key>
<string>0</string>
<key>HOME</key>
<string>/Users/YOUR_USERNAME</string>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/bin:/bin</string>
</dict>
<key>RunAtLoad</key>
<true/>
<key>KeepAlive</key>
<true/>
<key>StandardOutPath</key>
<string>/tmp/ollama.log</string>
<key>StandardErrorPath</key>
<string>/tmp/ollama.log</string>
</dict>
</plist>
2. ジョブの登録とモデルのプル
# ユーザーのLaunchAgentsディレクトリにコピー
cp com.local.ollama.plist ~/Library/LaunchAgents/
# 権限を適切に設定
chmod 644 ~/Library/LaunchAgents/com.local.ollama.plist
# macOS 12 (Monterey) 以降推奨のコマンドで登録
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.local.ollama.plist
# Ollamaのプロセス起動を数秒待機
sleep 3
# 起動確認(0.0.0.0:11434でバインドされているか検証)
curl http://localhost:11434
# 3Bクラスの軽量モデルをプル
ollama pull qwen2.5:3b
Step 5: 事前準備(初期化スクリプトとセキュアな環境変数)
n8nとDifyがDBを共有してテーブルが衝突するのを防ぐSQL初期化スクリプトと、コンテナ起動に必要な .env ファイルを生成します。
【重要】ホストLAN IPの固定化(Static DHCP)について
Dify Web UIのアクセスにはホストマシンのLAN IPを使用します。ルーター側の設定で、このMac miniのMACアドレスに対して**IPアドレスを固定(Static DHCP)**してください。DHCPでIPが変わるとDifyのUIが機能不全に陥ります。
以降の作業は、専用のディレクトリを作成してその中で行います。
mkdir -p ~/edge-server
cd ~/edge-server
ディレクトリ構成:
edge-server/
├── docker-compose.yml
├── init-db.sh
├── setup.sh
└── .env (setup.sh実行後に自動生成される)
1. データベース初期化スクリプトの作成 (init-db.sh)
#!/bin/bash
set -e
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" <<-EOSQL
CREATE DATABASE n8n_db;
CREATE DATABASE dify_db;
EOSQL
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname=dify_db <<-EOSQL
CREATE EXTENSION IF NOT EXISTS vector;
EOSQL
2. セットアップスクリプトの作成 (setup.sh)
#!/bin/bash
set -e
if [ ! -f init-db.sh ]; then
echo "Error: init-db.sh not found. Please create it first."
exit 1
fi
chmod +x init-db.sh
if [ -f .env ]; then
echo ".env already exists. Aborting to prevent overwriting."
exit 1
fi
# デフォルトルートに使われているインターフェースのIPを堅牢に取得
HOST_LAN_IP=$(route get default 2>/dev/null | awk '/interface:/{print $2}' | xargs -I{} ipconfig getifaddr {})
# VPN環境などでIPが取得できなかった場合の防衛線
if [ -z "$HOST_LAN_IP" ]; then
echo "Error: Could not detect LAN IP. Set HOST_LAN_IP manually in .env"
exit 1
fi
{
echo "N8N_ENCRYPTION_KEY=$(openssl rand -hex 32)"
echo "DIFY_SECRET_KEY=$(openssl rand -hex 32)"
echo "POSTGRES_PASSWORD=$(openssl rand -hex 16)"
echo "HOST_LAN_IP=${HOST_LAN_IP}"
} > .env
echo "Setup complete. .env file generated."
作成後、ターミナルで bash setup.sh を実行して準備を完了させます。
Step 6: docker-compose.yml の構築
【必須要件】
本設定ファイルの deploy.resources.limits(メモリ制限)を有効にするため、Docker Compose v2(docker compose コマンド)での実行が必須です。
【起動時の注意】
初回起動時は、dify-api コンテナ内でデータベースのマイグレーションが実行されます。この処理が完了するまで healthcheck はパスせず、依存している dify-worker 等の起動が待機状態になります。初回起動時は全体が立ち上がるまで2〜3分かかります。
services:
postgres:
image: pgvector/pgvector:0.8.0-pg16
networks:
- edge_net
restart: unless-stopped
deploy:
resources:
limits:
memory: 384M
reservations:
memory: 128M
command: >
postgres
-c shared_buffers=64MB
-c effective_cache_size=128MB
-c work_mem=4MB
-c max_connections=50
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=${POSTGRES_PASSWORD}
- POSTGRES_DB=postgres
volumes:
- postgres_data:/var/lib/postgresql/data
- ./init-db.sh:/docker-entrypoint-initdb.d/init.sh
healthcheck:
test: ["CMD-SHELL", "pg_isready -U postgres"]
interval: 10s
retries: 5
timeout: 5s
start_period: 15s
redis:
image: redis:7.2-alpine
networks:
- edge_net
restart: unless-stopped
# Celeryタスク(TTLなし)を保護し、キャッシュ(TTLあり)のみ退避するポリシー
command: redis-server --maxmemory 64mb --maxmemory-policy volatile-lru
deploy:
resources:
limits:
memory: 96M
volumes:
- redis_data:/data
healthcheck:
test: ["CMD", "redis-cli", "ping"]
interval: 10s
retries: 5
timeout: 5s
n8n:
image: n8nio/n8n:1.80.0
networks:
- edge_net
restart: unless-stopped
ports:
- "5678:5678"
depends_on:
postgres:
condition: service_healthy
deploy:
resources:
limits:
memory: 512M
environment:
- NODE_OPTIONS=--max-old-space-size=384
- N8N_ENCRYPTION_KEY=${N8N_ENCRYPTION_KEY}
- WEBHOOK_URL=http://${HOST_LAN_IP}:5678/
- GENERIC_TIMEZONE=Asia/Tokyo
- TZ=Asia/Tokyo
- DB_TYPE=postgresdb
- DB_POSTGRESDB_HOST=postgres
- DB_POSTGRESDB_PORT=5432
- DB_POSTGRESDB_USER=postgres
- DB_POSTGRESDB_PASSWORD=${POSTGRES_PASSWORD}
- DB_POSTGRESDB_DATABASE=n8n_db
volumes:
- n8n_data:/home/node/.n8n
healthcheck:
test: ["CMD-SHELL", "wget -qO- http://localhost:5678/healthz || exit 1"]
interval: 10s
retries: 5
timeout: 5s
start_period: 30s
dify-api:
image: langgenius/dify-api:0.15.3
networks:
- edge_net
restart: unless-stopped
ports:
- "5001:5001"
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
deploy:
resources:
limits:
memory: 768M
environment:
- MODE=api
- MIGRATION_ENABLED=true
- SERVER_WORKER_AMOUNT=2
- GUNICORN_TIMEOUT=360
- SECRET_KEY=${DIFY_SECRET_KEY}
- CONSOLE_WEB_URL=http://${HOST_LAN_IP}:3000
- APP_WEB_URL=http://${HOST_LAN_IP}:3000
- FILES_URL=http://${HOST_LAN_IP}:5001
- FILES_ACCESS_TIMEOUT=300
- REDIS_HOST=redis
- REDIS_PORT=6379
- CELERY_BROKER_URL=redis://redis:6379/1
- DB_USERNAME=postgres
- DB_PASSWORD=${POSTGRES_PASSWORD}
- DB_HOST=postgres
- DB_PORT=5432
- DB_DATABASE=dify_db
- VECTOR_STORE=pgvector
- PGVECTOR_HOST=postgres
- PGVECTOR_PORT=5432
- PGVECTOR_USER=postgres
- PGVECTOR_PASSWORD=${POSTGRES_PASSWORD}
- PGVECTOR_DATABASE=dify_db
- STORAGE_TYPE=local
- STORAGE_LOCAL_PATH=/app/api/storage
volumes:
- dify_storage:/app/api/storage
healthcheck:
test: ["CMD-SHELL", "curl -sf http://localhost:5001/console/api/setup -o /dev/null || exit 1"]
interval: 15s
retries: 5
timeout: 5s
start_period: 60s
dify-worker:
image: langgenius/dify-api:0.15.3
networks:
- edge_net
restart: unless-stopped
command: celery -A app.celery worker -P gevent -c 1 -Q dataset,generation,mail
depends_on:
postgres:
condition: service_healthy
redis:
condition: service_healthy
dify-api:
condition: service_healthy
deploy:
resources:
limits:
memory: 512M
environment:
- MODE=worker
- SECRET_KEY=${DIFY_SECRET_KEY}
- CONSOLE_WEB_URL=http://${HOST_LAN_IP}:3000
- APP_WEB_URL=http://${HOST_LAN_IP}:3000
- FILES_URL=http://${HOST_LAN_IP}:5001
- FILES_ACCESS_TIMEOUT=300
- REDIS_HOST=redis
- REDIS_PORT=6379
- CELERY_BROKER_URL=redis://redis:6379/1
- DB_USERNAME=postgres
- DB_PASSWORD=${POSTGRES_PASSWORD}
- DB_HOST=postgres
- DB_PORT=5432
- DB_DATABASE=dify_db
- VECTOR_STORE=pgvector
- PGVECTOR_HOST=postgres
- PGVECTOR_PORT=5432
- PGVECTOR_USER=postgres
- PGVECTOR_PASSWORD=${POSTGRES_PASSWORD}
- PGVECTOR_DATABASE=dify_db
- STORAGE_TYPE=local
- STORAGE_LOCAL_PATH=/app/api/storage
volumes:
- dify_storage:/app/api/storage
dify-web:
image: langgenius/dify-web:0.15.3
networks:
- edge_net
restart: unless-stopped
depends_on:
dify-api:
condition: service_healthy
deploy:
resources:
limits:
memory: 256M
environment:
- CONSOLE_API_URL=http://${HOST_LAN_IP}:5001
- APP_API_URL=http://${HOST_LAN_IP}:5001
ports:
- "3000:3000"
networks:
edge_net:
driver: bridge
volumes:
postgres_data:
redis_data:
n8n_data:
dify_storage:
Step 7: 安全なLaunchd設定(自動クリーンアップ)
未使用のDockerイメージを週に1回自動クリーンアップします。
launchd はユーザーの環境変数(PATH)を継承しないため、明示的に環境変数を定義した plist ファイルを作成し、ユーザー領域である ~/Library/LaunchAgents/ に配置します。
YOUR_USERNAME の部分はご自身のMacのユーザー名に書き換えてください。
1. com.local.docker-cleanup.plist の作成
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.local.docker-cleanup</string>
<key>EnvironmentVariables</key>
<dict>
<key>HOME</key>
<string>/Users/YOUR_USERNAME</string>
<key>PATH</key>
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
</dict>
<key>ProgramArguments</key>
<array>
<string>/bin/sh</string>
<string>-c</string>
<string>/opt/homebrew/bin/docker system prune -f --filter "until=168h" >> /tmp/docker-cleanup.log 2>&1 || true</string>
</array>
<key>StartCalendarInterval</key>
<dict>
<key>Hour</key><integer>3</integer>
<key>Minute</key><integer>0</integer>
<key>Weekday</key><integer>0</integer>
</dict>
</dict>
</plist>
2. plistの配置と有効化
# ユーザーのLaunchAgentsディレクトリにコピー
cp com.local.docker-cleanup.plist ~/Library/LaunchAgents/
# 権限を適切に設定
chmod 644 ~/Library/LaunchAgents/com.local.docker-cleanup.plist
# ジョブをロードして有効化
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.local.docker-cleanup.plist
Step 8: API経由のメモリ解放アーキテクチャ
Ollama v0.3以降の仕様を利用し、n8nからのAPIコール時のみモデルをロードし、直後にメモリからアンロードさせます。
※Step 4で OLLAMA_KEEP_ALIVE=0 のグローバル設定を行っているため、APIコール側での記載漏れがあってもフェイルセーフが働きます。
【n8nでのHTTP Requestノード設定】
-
URL:
http://host.docker.internal:11434/api/generate - Method: POST
-
Body (JSON):
{ "model": "qwen2.5:3b", "prompt": "推論プロンプト...", "stream": false, "keep_alive": 0 }
Step 9: デプロイ完了と初回セットアップ
# 作成した edge-server ディレクトリ内で実行
cd ~/edge-server
docker compose up -d
# コンテナの起動状態を確認
docker compose ps
# 【確認ポイント】
# postgres, redis, n8n, dify-api → STATUS: "healthy" になるまで待機
# dify-worker, dify-web → healthcheck未設定のため STATUS: "running" で正常です
# dify-api のワーカー数を確認("2" と表示されれば SERVER_WORKER_AMOUNT=2 が有効)
docker compose logs dify-api 2>&1 | grep -i "booting worker" | wc -l
# リアルタイムで全体のログを確認する場合(初回はマイグレーションで数分かかります)
docker compose logs -f
全コンテナが正常に起動(healthy/running)したことを確認したら、以下のURLにアクセスして初期設定を行います。
-
Dify UI:
http://[Mac miniのLAN IP]:3000- 初回アクセス時に管理者アカウントの作成ウィザードが表示されます。
-
n8n UI:
http://[Mac miniのLAN IP]:5678- 初回アクセス時にオーナーアカウントの設定が求められます。
各ツール内で、host.docker.internal:11434 を指定することで、Mac上のローカルLLMを外部API(OpenAI等)の代替として組み込むことが可能になります。
この記事を書いた人✏️@YushiYamamoto
ITPRODX.com代表 / AIアーキテクト
Next.js / TypeScript / n8nを活用した自律型アーキテクチャ設計を専門としています。
日々の自動化の検証結果や、ビジネス側の視点(ROI等)に関するより深い考察は、以下の公式サイトおよびnoteで発信しています。
