Obsidian + ラズパイ + Web UI 実装ガイド|ノートをどこからでも見られる環境を作ろう

ノートを手元のPCだけで管理していると、外出先で「あのメモどこだっけ?」という場面が必ず来ます。

クラウドサービスを使えば解決できますが、「データを自分で管理したい」「無料でやりたい」「ついでに仕組みも理解したい」という人には、ラズパイ + Obsidian + Web UI という構成がかなりいい選択肢になります。

この記事では、実際に動かせるコードを交えながら、セットアップ方法を順番にまとめてみました。


概要 — なぜこのセットアップが有効なのか

ObsidianはローカルファイルベースのMarkdownノートアプリです。クラウドに依存しないぶん、「ネットがないと使えない」問題とは無縁ですが、逆に複数端末・外出先からのアクセスが少し手間になります。

ラズパイを自宅サーバーとして動かし、Obsidian VaultをそこにマウントしてWeb UIを立てると:

  • スマホ・タブレット・外部PCからブラウザだけでノートにアクセスできる
  • 月額コストゼロ(電気代のみ)
  • データが自宅サーバーに残るのでプライバシーも確保できる
  • Claude Code と連携してノートの自動生成・分析もできる

サブスクに月数百〜数千円払うくらいなら、一度作ってしまったほうが長期的にお得かもしれません。


アーキテクチャ図 — 全体像を視覚化

[スマホ / 外部PC]
      │
      │ HTTPS (Cloudflare Tunnel)
      ▼
[ラズパイ (自宅)]
  ├─ Obsidian Vault (Markdownファイル群)
  ├─ Web サーバー (Node.js or Python)
  └─ Claude Code CLI
      │
      ▼
  Web UI (ブラウザでノート閲覧・検索・編集)

ポイントは Cloudflare Tunnel です。自宅のルーターにポート開放せずに外部からアクセスできるので、セキュリティ的にも安心できます。


ラズパイのセットアップ

必要なもの

  • Raspberry Pi 4 または 5(2GB RAM以上推奨)
  • microSDカード(32GB以上)
  • Raspberry Pi OS(64bit Lite版がおすすめ)

Node.js のインストール

# NodeSourceのリポジトリを使う(LTS版)
curl -fsSL https://deb.nodesource.com/setup_lts.x | sudo -E bash -
sudo apt install -y nodejs

# バージョン確認
node -v
npm -v

Python のインストール

最近のRaspberry Pi OSにはPython3が入っていますが、バージョンを確認しておきます。

python3 --version

# pip のアップデート
pip3 install --upgrade pip

# 仮想環境を作っておくと管理が楽
python3 -m venv ~/venv
source ~/venv/bin/activate

Git のインストール

sudo apt install -y git
git config --global user.name "yourname"
git config --global user.email "your@email.com"

Vault の構成 — ファイル構造

ラズパイ上のVaultは、以下のような構成にしておくと管理しやすいです。

~/obsidian-vault/
├── 00_inbox/          ← とりあえずここに放り込む
├── 10_projects/       ← 進行中プロジェクト
├── 20_knowledge/      ← 技術メモ・学習ログ
├── 30_daily/          ← デイリーノート
├── 90_archive/        ← 完了・不要になったもの
└── .obsidian/         ← 設定ファイル(自動生成)

既存のVaultをそのまま持ってくる場合はrsyncかgitで同期します。

# PCからラズパイへ初回コピー
rsync -avz ~/Documents/Obsidian/ pi@192.168.x.x:~/obsidian-vault/

# 以降の同期(差分のみ)
rsync -avz --delete ~/Documents/Obsidian/ pi@192.168.x.x:~/obsidian-vault/

Web サーバー実装

Node.js 版

// server.js
const express = require('express');
const fs = require('fs');
const path = require('path');
const marked = require('marked');

const app = express();
const VAULT_PATH = process.env.VAULT_PATH || '/home/pi/obsidian-vault';

app.use(express.static('public'));

// ノート一覧
app.get('/api/notes', (req, res) => {
  const notes = [];
  const walk = (dir) => {
    fs.readdirSync(dir).forEach(file => {
      const fullPath = path.join(dir, file);
      if (fs.statSync(fullPath).isDirectory()) {
        walk(fullPath);
      } else if (file.endsWith('.md')) {
        notes.push(fullPath.replace(VAULT_PATH + '/', ''));
      }
    });
  };
  walk(VAULT_PATH);
  res.json(notes);
});

// ノート取得(HTMLに変換)
app.get('/api/note', (req, res) => {
  const filePath = path.join(VAULT_PATH, req.query.path);
  if (!filePath.startsWith(VAULT_PATH)) {
    return res.status(403).json({ error: 'Forbidden' });
  }
  const md = fs.readFileSync(filePath, 'utf-8');
  res.json({ html: marked.parse(md), raw: md });
});

app.listen(3000, () => console.log('Server running on port 3000'));
# インストールと起動
npm init -y
npm install express marked
node server.js

Python 版(FastAPI)

# server.py
from fastapi import FastAPI, HTTPException
from fastapi.staticfiles import StaticFiles
from pathlib import Path
import markdown

app = FastAPI()
VAULT_PATH = Path("/home/pi/obsidian-vault")

@app.get("/api/notes")
def list_notes():
    notes = [str(p.relative_to(VAULT_PATH)) for p in VAULT_PATH.rglob("*.md")]
    return notes

@app.get("/api/note")
def get_note(path: str):
    file_path = VAULT_PATH / path
    if not str(file_path).startswith(str(VAULT_PATH)):
        raise HTTPException(status_code=403, detail="Forbidden")
    if not file_path.exists():
        raise HTTPException(status_code=404, detail="Not found")
    md = file_path.read_text()
    html = markdown.markdown(md, extensions=["fenced_code", "tables"])
    return {"html": html, "raw": md}

app.mount("/", StaticFiles(directory="public", html=True), name="static")
pip install fastapi uvicorn markdown
uvicorn server:app --host 0.0.0.0 --port 3000

Web UI — HTML/CSS/JS の実装例

public/index.html を作ります。

<!DOCTYPE html>
<html lang="ja">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Obsidian Web UI</title>
  <style>
    * { box-sizing: border-box; margin: 0; padding: 0; }
    body { font-family: sans-serif; display: flex; height: 100vh; }
    #sidebar {
      width: 260px; background: #1e1e2e; color: #cdd6f4;
      overflow-y: auto; padding: 1rem;
    }
    #sidebar input {
      width: 100%; padding: 0.4rem; margin-bottom: 0.8rem;
      background: #313244; color: #cdd6f4; border: none; border-radius: 4px;
    }
    #note-list li {
      list-style: none; padding: 0.3rem 0.5rem;
      cursor: pointer; border-radius: 4px; font-size: 0.85rem;
    }
    #note-list li:hover { background: #313244; }
    #content {
      flex: 1; padding: 2rem; overflow-y: auto; background: #1e1e2e; color: #cdd6f4;
    }
    #content h1, h2, h3 { margin: 1.5rem 0 0.5rem; color: #89b4fa; }
    #content pre { background: #313244; padding: 1rem; border-radius: 6px; overflow-x: auto; }
    #content code { font-family: monospace; font-size: 0.9em; }
    #content p { line-height: 1.8; margin-bottom: 1rem; }
  </style>
</head>
<body>
  <div id="sidebar">
    <input type="text" id="search" placeholder="検索...">
    <ul id="note-list"></ul>
  </div>
  <div id="content"><p>← ノートを選んでください</p></div>

  <script>
    let allNotes = [];

    async function loadNoteList() {
      const res = await fetch('/api/notes');
      allNotes = await res.json();
      renderList(allNotes);
    }

    function renderList(notes) {
      const ul = document.getElementById('note-list');
      ul.innerHTML = notes.map(n =>
        `<li onclick="loadNote('${n}')">${n.replace(/\.md$/, '')}</li>`
      ).join('');
    }

    async function loadNote(path) {
      const res = await fetch(`/api/note?path=${encodeURIComponent(path)}`);
      const data = await res.json();
      document.getElementById('content').innerHTML = data.html;
    }

    document.getElementById('search').addEventListener('input', e => {
      const q = e.target.value.toLowerCase();
      renderList(allNotes.filter(n => n.toLowerCase().includes(q)));
    });

    loadNoteList();
  </script>
</body>
</html>

外部アクセス — Cloudflare Tunnel

ポート開放なしで外部からアクセスできる仕組みです。

インストール(ラズパイ)

# ARM64 版をダウンロード
wget https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-arm64.deb
sudo dpkg -i cloudflared-linux-arm64.deb

# Cloudflare にログイン(ブラウザが開く)
cloudflared tunnel login

# トンネル作成
cloudflared tunnel create obsidian-vault

# config.yml を作成
mkdir -p ~/.cloudflared
cat > ~/.cloudflared/config.yml <<EOF
tunnel: <TUNNEL_ID>
credentials-file: /home/pi/.cloudflared/<TUNNEL_ID>.json
ingress:
  - hostname: vault.yourdomain.com
    service: http://localhost:3000
  - service: http_status:404
EOF

# DNSレコード追加
cloudflared tunnel route dns obsidian-vault vault.yourdomain.com

# 起動
cloudflared tunnel run obsidian-vault

systemd に登録しておくと再起動後も自動起動します。

sudo cloudflared service install
sudo systemctl enable cloudflared
sudo systemctl start cloudflared

Claude Code との統合

ラズパイ上でClaude Code CLIを使うと、ノートの自動生成・分析ができます。

インストール

npm install -g @anthropic-ai/claude-code

ノート自動生成の例

# 今日のデイリーノートを自動生成
DATE=$(date +%Y-%m-%d)
claude "今日の日付は${DATE}です。デイリーノートのテンプレートを作って ~/obsidian-vault/30_daily/${DATE}.md に保存してください。タスク欄・振り返り欄を含めてください。"

ノート分析の例

# プロジェクトフォルダのノートを要約
claude "~/obsidian-vault/10_projects/ にあるMarkdownファイルを読んで、進行中プロジェクトの一覧と各プロジェクトの状況を箇条書きでまとめてください。"

Webhook と組み合わせる

Web UIにボタンを追加して、ブラウザからClaude Codeを呼び出す構成も作れます。

// server.js に追加
const { exec } = require('child_process');

app.post('/api/claude', (req, res) => {
  const { prompt } = req.body;
  exec(`claude "${prompt.replace(/"/g, '\\"')}"`, (err, stdout) => {
    res.json({ result: stdout });
  });
});

セキュリティ

認証を追加する(Basic認証)

// Node.js 版
const basicAuth = require('express-basic-auth');

app.use(basicAuth({
  users: { 'admin': process.env.AUTH_PASSWORD },
  challenge: true
}));
npm install express-basic-auth
AUTH_PASSWORD=yourpassword node server.js

HTTPS(Cloudflare Tunnel を使っている場合)

Cloudflare Tunnel 経由でアクセスする場合、Cloudflare側でHTTPS終端されるため、ラズパイ内部はHTTPのままでもブラウザには鍵マークが付きます。

定期バックアップ

# crontab に追加(毎日深夜2時にバックアップ)
0 2 * * * rsync -avz /home/pi/obsidian-vault/ /home/pi/backup/obsidian-$(date +%Y%m%d)/

古いバックアップを自動削除するスクリプトも合わせて設定しておくと安心です。

# 7日以上前のバックアップを削除
find /home/pi/backup/ -name "obsidian-*" -mtime +7 -exec rm -rf {} +

トラブルシューティング

ポートが開かない

# ポートの使用状況確認
sudo ss -tlnp | grep 3000

# ファイアウォール確認
sudo ufw status
sudo ufw allow 3000

Cloudflare Tunnel が接続できない

# ログ確認
sudo journalctl -u cloudflared -f

# 再認証
cloudflared tunnel login

Markdownが正しく表示されない

フロントマター(---で囲まれた部分)が本文に混じって表示される場合は、パース時に除外する処理を追加します。

function stripFrontmatter(md) {
  return md.replace(/^---[\s\S]*?---\n/, '');
}

ラズパイが重い

Node.jsよりPythonのFastAPIのほうがメモリ消費が少ない傾向があります。また、markedmarkdown の変換をキャッシュすることで負荷を減らせます。


まとめ

Obsidian + ラズパイ + Web UI の構成は、一度作ってしまえば:

  • ノートがどこからでも見られる
  • データは自宅に残る
  • Claude Code で自動化も効く

という状態になります。初期セットアップに数時間かかりますが、長く使い続けることを考えると十分元が取れると思います。

ラズパイがない場合は、古いPCやVPS(月数百円)でも同じ構成が作れます。まずは手元の環境で試してみてください。

関連記事

ブログ

BLOG

PAGE TOP