k8sでDockerイメージを格納するためのプライベートレジストリを構築する

こんにちは、ナナオです。 自宅にk8sを構築してからいろいろやりたいことが増えました。 ということで今回はk8sを使って自宅内で使えるプライベートレジストリの構築に挑戦してみました。 どうやって構築するか うちには以前ジャンクで買ったNASがあるので、このNASをストレージにしてDockerイメージを放り込めるようにしたいです。 NASにはNFSで接続できます。 一度NAS側にDockerイメージを放り込むためのディレクトリを作成しておきます。 私の場合WebUIで設定できました 次に先ほど作成したディレクトリを参照するPVとPVCを作っていきます。 apiVersion: v1 kind: PersistentVolume metadata: name: registry-pv spec: capacity: storage: 100Gi accessModes: - ReadWriteMany # 複数ノードから同時に読み書き可能 persistentVolumeReclaimPolicy: Retain storageClassName: nas nfs: server: 192.168.0.xx # NASのIPアドレス path: /docker-registry # NASの共有パス --- apiVersion: v1 kind: PersistentVolumeClaim metadata: name: registry-pvc namespace: kube-system spec: accessModes: - ReadWriteMany resources: requests: storage: 100Gi storageClassName: nas 作成したところ、エラーになりました。 │ Warning FailedScheduling 15m default-scheduler 0/3 nodes are available: persistentv │ │ olumeclaim "registry-pvc" not found. not found │ │ Warning FailedScheduling 15m default-scheduler running PreFilter plugin "VolumeBind │ │ ing": error getting PVC "kube-system/registry-pvc": could not find v1.PersistentVolumeClaim "kube-system │ │ /registry-pvc" │ │ Normal Scheduled 15m default-scheduler Successfully assigned kube-system/do │ │ cker-registry-85dbb54f6c-zmrkl to nanaonuc6caysserver │ │ Warning FailedMount 58s (x15 over 15m) kubelet MountVolume.SetUp failed for volume │ │ "registry-pv" : mount failed: exit status 32 │ │ Mounting command: mount │ │ Mounting arguments: -t nfs 192.168.0.94:/docker-registry /var/lib/kubelet/pods/1f2e1d4d-447c-4bcf-9987-c │ │ 930a2576654/volumes/kubernetes.io~nfs/registry-pv │ │ Output: mount: /var/lib/kubelet/pods/1f2e1d4d-447c-4bcf-9987-c930a2576654/volumes/kubernetes.io~nfs/regi │ │ stry-pv: bad option; for several filesystems (e.g. nfs, cifs) you might need a /sbin/mount.<type> helper │ │ program. │ │ dmesg(1) may have more information after failed mount system call. エラーのbad option; for several filesystems (e.g. nfs, cifs) you might need a /sbin/mount.<type> helperから、NFSを扱うためのツールセット各ノードにがインストールされていないことが原因だとわかりました。 ...

2026年1月11日 · にあえん

おうちでk3sを使ってk8sを構築する

こんにちは、ナナオです。 今日は家でk8sを構築した際のことを書いていこうと思います。 経緯 最近、よくハードオフ巡り(通称: ハドフ巡り)をしています。 というのも、ジャンクPCが好きなんですよね。 手ごろな価格のPCを見つけては、意味もなくUbuntu載せたりメモリ乗せ換えたりして、動かすようにするのが楽しいんです。 ただ、サーバーは増えてるのですが肝心の何に使うかっていうのが決まってないんですよね。(何のために増やしてんねん!) ということで、最近家で動かしておきたいサービスも増えたので(これとか)、この機会に家にk8sのノードを構築していこうと思います。 実装 まずは家にサーバーを建てます。 今回はジャンクで買ってきたPCを三台ほど使います。 デスク下に配置したPC。汚くてすみません。。 OSはUbuntu Server24.04を入れて、ssh接続できるところまでは確認済みです。 ここにk8sディストリビューションとして(k3s)[https://k3s.io/]を導入します。 k3sはとにかく軽量なk8sディストリビューションで、また導入コストも低いということでこれにしました。 また、可用性を考慮してコントロールプレーンを複数台構築して、ワーカーノードは後付けするようにしました。 まずは一台目に以下のコマンドを実行します。 手順はこちらのドキュメントを参照します。 curl -sfL https://get.k3s.io | sh - 構築出来たら早速システムのポッドを確認してみましょう。 $ kubectl get pod --namespace kube-system WARN[0000] Unable to read /etc/rancher/k3s/k3s.yaml, please start server with --write-kubeconfig-mode or --write-kubeconfig-group to modify kube config permissions error: error loading config file "/etc/rancher/k3s/k3s.yaml": open /etc/rancher/k3s/k3s.yaml: permission denied あら、権限エラーが出力されました。 ということで、sudoをつけて再度トライすると… $ sudo kubectl get pod --namespace kube-system NAME READY STATUS RESTARTS AGE coredns-7f496c8d7d-cdzf8 1/1 Running 0 8m32s helm-install-traefik-5ptvv 0/1 Completed 0 8m31s helm-install-traefik-crd-rdkbj 0/1 Completed 0 8m32s local-path-provisioner-578895bd58-6ntrr 1/1 Running 0 8m32s metrics-server-7b9c9c4b9c-brdnd 1/1 Running 0 8m32s svclb-traefik-74f79865-phf72 2/2 Running 0 8m28s traefik-6f5f87584-gqqvf 1/1 Running 0 8m13s ということで無事出力されました。 ...

2026年1月10日 · にあえん

k8sでクラスタ内アクセスするときのURL

k8s使っている人からしたら当たり前っちゃ当たり前かもしれないですが、備忘録として残しておきます。 URL 以下のような形式になっています。 <Service名>.<Namespace名>.svc.cluster.local 感想 思い出したときはここを見よう

2026年1月9日 · にあえん

streamlitで表を出力した

こんにちは、ナナオです。 前回の記事で図書館の予約管理システムを構築しました。 データ取得まではできましたが、データ出力するところが実装できなかったので今回実装していこうと思います。 React使うか~とかも思ったのですが、Geminiと相談した結果Streamlitというライブラリを使うことにしました。 Streamlitとは グラフデータや表データなどを含むWebアプリを超簡単に構築できるライブラリです。 Streamlit • A faster way to build and share data apps もちろん自由度はReactでフロントエンドを作るよりも低くはなるのですが、大体の機能が揃っているのでとりあえずデータ出力したいだけであればこれで十分だと思います。 実装 ということで実装していきます。 今回は以下のデータクラスを表で出力します。 @dataclass class LoanStatusItem: deadline_at: dt.date borrower_name: str library_name: str book_title: str @dataclass class ReserveItem: status: Literal["reserved", "already"] borrower_name: str library_name: str book_title: str 必要なライブラリを追加します。 uv add streamlit pandas 実装はとりあえずダミーデータを使ってやりました。 def get_dummy_data(): # 貸出状況のデータ loans = [ LoanStatusItem(dt.date(2023, 10, 15), "佐藤", "中央図書館", "Python入門"), LoanStatusItem(dt.date(2023, 10, 20), "鈴木", "北図書館", "Streamlit実践"), LoanStatusItem(dt.date(2023, 10, 12), "佐藤", "中央図書館", "機械学習の基礎"), ] # 予約状況のデータ reserves = [ ReserveItem("reserved", "佐藤", "南図書館", "Web設計パターン"), ReserveItem("already", "田中", "中央図書館", "デザイン思考"), ReserveItem("reserved", "鈴木", "北図書館", "Docker活用"), ] current_time = dt.datetime.now().strftime('%H:%M:%S') return loans, reserves, current_time def main(): st.title("図書館 利用状況ダッシュボード") # --- 更新ボタンの配置 --- # col1, col2 を使うことで、タイトルやボタンの配置を調整できます col1, col2 = st.columns([3, 1]) with col1: st.write("最新の貸出・予約状況を表示します。") with col2: # ボタンが押されたらキャッシュをクリアして再実行 if st.button("最新情報を取得 🔄"): st.cache_data.clear() # キャッシュを削除 st.rerun() # スクリプトを再実行(画面更新) # データの取得 loans, reserves, fetched_time = get_dummy_data() # 更新時刻の表示 st.caption(f"データ取得時刻: {fetched_time}") # --- 1. 貸出状況の表示 --- st.subheader("📅 貸出状況 (Loan Status)") if loans: # dataclassのリストをDataFrameに変換 # dataclasses.asdictを使うと辞書に変換され、DataFrame化しやすくなります df_loans = pd.DataFrame([asdict(item) for item in loans]) # カラム名の見た目を整える(任意) df_loans = df_loans.rename(columns={ "deadline_at": "返却期限", "borrower_name": "利用者名", "library_name": "図書館", "book_title": "書名" }) # テーブル表示 (use_container_width=Trueで横幅いっぱいに表示) st.dataframe(df_loans, use_container_width=True) else: st.info("貸出中の本はありません。") st.markdown("---") # 区切り線 # --- 2. 予約状況の表示 --- st.subheader("🔖 予約状況 (Reserve Status)") if reserves: df_reserves = pd.DataFrame([asdict(item) for item in reserves]) # カラム名の整理 df_reserves = df_reserves.rename(columns={ "status": "状態", "borrower_name": "利用者名", "library_name": "図書館", "book_title": "書名" }) # 状態(reserved/already)に応じて色をつけたりも可能です st.dataframe(df_reserves, use_container_width=True) else: st.info("予約中の本はありません。") if __name__ == "__main__": main() これを実行します。 ...

2026年1月8日 · にあえん

Seleniumで図書館の予約管理システムを作った

こんにちは、ナナオです。 最近、いろんな図書館からいろんな本を借りることが多いのですが、書籍の予約管理を今はメモで行っており、ちょっとこれだと見づらいので一元管理できるシステムを作ることにしました。 ということで、早速実装していきましょう。 (Seleniumの詳細な説明は省略します、詳細はこちらから!) 仕様 とりあえず必要な仕様は以下の通りです。最初はシンプルにいきましょう。 WebUIを出力 書籍の予約を一覧で表示する 「名前」と「借りている図書館」を表示する とりあえずこれでいきましょう。 実装にはSeleniumと相性がよく、私がすぐ実装できるpythonを使用します。 実装 とりあえずローカルでseleniumを動作させられる環境を構築します。 まずは土台になるパッケージを作成します。 uvを使います。 uv init --package --project library-checker uv add selenium seleniumの動作環境構築はDockerでやるのが一番楽なので、以下のコマンドを実行します。 docker run -d -p 4444:4444 --rm --shm-size="2g" selenium/standalone-chrome:4.39.0-20251212 (実行イメージはこちらを参考にしました) これでとりあえず動作させる準備が整いました。 seleniumを動作させる基本実装は以下の通りです。 from selenium import webdriver def main(): # Chrome のオプションを設定する options = webdriver.ChromeOptions() options.add_argument('--headless') # Selenium Server に接続する driver = webdriver.Remote( command_executor='http://localhost:4444/wd/hub', options=options, ) # ブラウザを終了する driver.quit() if __name__ == "__main__": main() これで特にエラーなく動作すれば最初のステップは問題ないです。 ...

2026年1月7日 · にあえん

Disqus使ってhugoのPaperModにコメントできるようにした

こんにちは、ナナオです。 今回はこのブログにコメント機能を導入したいと思います。 とはいえ、このブログ自体はHugoで構築しており、動的サイトではないのでdisqusというサービスを使ってコメント機能を埋め込もうと思います。 まぁ大体の手順はこちらに書いてあります。 Hugo で作ったブログに Disqus を使ってコメント機能を追加する - michimani.net ここではこのブログに使っているテーマのPaperModでコメント機能を埋め込むための方法を書いておこうと思います。 方法 まずはPaperModのlayouts/partials/comment.htmlを以下のように編集します。 {{- /* Comments area start */ -}} {{- /* to add comments read => https://gohugo.io/content-management/comments/ */ -}} {{ template "_internal/disqus.html" . }} {{- /* Comments area end */ -}} {{ template "_internal/disqus.html" . }}を追加しています。 また、hugoの設定ファイルも編集します。 disqusShortname = "neer-engineer-com" [params] # ...中略... comments = true これでOKです。 感想 disqusでコメントくれるとうれしいです。 参考 [BUG] Comments section not appearing on post page · Issue #502 · adityatelange/hugo-PaperMod · GitHub

2026年1月6日 · にあえん

100行でアービトラージ監視Botを作った

こんにちは。ナナオです。 アービトラージに興味があり、開発してみたいな~と思いつつなかなか手が出せなかったのですが、この度重い腰をあげて開発してみました。 使用技術 監視にはPrometheus + Grafanaを使用しました。 実装はPythonを使い、HTTPリクエストにrequestsを使っています。 実装 早速実装です。 メインになる実装は以下の通りです。 ここの実装が大体100行くらいです。 import time import logging import threading from itertools import combinations # リクエストに使うAPI(自作) from vc_bot import exchange_api from prometheus_client import start_http_server, Gauge, Counter # 必要に応じてログの設定(これは標準エラー出力に出す設定です) logging.basicConfig(level=logging.ERROR) logger = logging.getLogger(__name__) PROFIT_GAUGE = Gauge("profit", "利益", ["exchange", "symbol"]) PROFIT_RATE_GAUGE = Gauge("profit_rate", "利益率", ["exchange", "symbol"]) REQUEST_ERROR_GAUGE = Counter("request_error", "リクエストエラー", ["exchange"]) def worker(): # 対象の取引所 exchange_pair = { "BTC_JPY": ["gmo", "coincheck", "binance", "bitflyer", "zaif", "bitbank", "okcoin"], "ETH_JPY": ["gmo", "coincheck", "binance", "bitflyer", "zaif", "bitbank", "okcoin"], "XRP_JPY": ['gmo', 'coincheck', 'binance', 'bitflyer', 'bitbank', 'okcoin'], "MONA_JPY": ['coincheck', 'bitflyer', 'zaif', 'bitbank'], } while True: for pair, exchanges in exchange_pair.items(): prices = {} # リクエストが成功した取引所のみ格納 success_exchanges = [] for exchange in exchanges: try: api = getattr(exchange_api, exchange)() prices.update({exchange: api.fetch_ticker(pair=pair)}) success_exchanges += [exchange] except Exception: logger.exception("リクエスト中にエラーが発生しました") REQUEST_ERROR_GAUGE.labels( exchange=exchange, ).inc() continue # 各取引所の比較結果を格納するGaugeオブジェクトを初期化 exchange_combination = list(combinations(success_exchanges, 2)) for exchange1, exchange2 in exchange_combination: ex1 = prices[exchange1] ex2 = prices[exchange2] # パターンA: Ex1で買って(Ask)、Ex2で売る(Bid) profit_a = ex2["bid"] - ex1["ask"] profit_rate_a = (profit_a / ex1["ask"]) * 100 print(f"{pair} {exchange1}-{exchange2} profit : {profit_a}") print(f"{pair} {exchange1}-{exchange2} profit rate: {profit_rate_a}") PROFIT_GAUGE.labels( exchange=f"{exchange1}_{exchange2}", symbol=pair ).set(profit_a) PROFIT_RATE_GAUGE.labels( exchange=f"{exchange1}_{exchange2}", symbol=pair ).set(profit_rate_a) # パターンB: Ex2で買って(Ask)、Ex1で売る(Bid) profit_b = ex1["bid"] - ex2["ask"] profit_rate_b = (profit_a / ex2["ask"]) * 100 print(f"{pair} {exchange2}-{exchange1} profit : {profit_b}") print(f"{pair} {exchange2}-{exchange1} profit rate: {profit_rate_b}") PROFIT_GAUGE.labels( exchange=f"{exchange2}_{exchange1}", symbol=pair ).set(profit_b) PROFIT_RATE_GAUGE.labels( exchange=f"{exchange2}_{exchange1}", symbol=pair ).set(profit_rate_b) def main(): # Prometheus ExporterのHTTPサーバーをポート8000で起動 start_http_server(8000) # メトリクス更新ワーカーを別スレッドで実行 worker_thread = threading.Thread(target=worker, daemon=True) worker_thread.start() print("Prometheus metrics server running on port 8000") # メインスレッドを維持 try: while True: time.sleep(1) except KeyboardInterrupt: print("Exiting.") if __name__ == "__main__": main() exchange_apiの実装は以下の通りです。 ...

2026年1月5日 · にあえん

React + Peerjsでグループチャットを実装してみる【2】

こんにちは。ナナオです。 前回の記事ではグループチャット実装のはじめの一歩を行いました。 今回も引き続き実装していこうと思います。 現在の実装の問題点 現在の実装から感じる問題点は以下の通りです。 メンバーの退出が検出されないケースがある メンバーの名前が設定できない 退出するとき、だれが出るのか分からない それぞれ修正していきます。 メンバーの退出が検出されないケースがある ブラウザを閉じた際などに退出したことがハンドリングされず、ほかの参加メンバーに退出したことが伝わらないことがありました。 そこで以下のようにハンドラを追加しました。 // --- タブを閉じる時に明示的に切断するハンドラ --- useEffect(() => { const handleBeforeUnload = () => { Object.values(connsRef.current).forEach(conn => { conn.send({ type: 'leave', peerId: myId }); // 退出を通知 conn.close(); }); peerRef.current?.destroy(); }; window.addEventListener('beforeunload', handleBeforeUnload); return () => { window.removeEventListener('beforeunload', handleBeforeUnload); peerRef.current?.destroy(); }; }, []); const setupConnection = (conn: DataConnection) => { // 定期的な接続チェック const checkInterval = setInterval(() => { if (!conn.open) { cleanup(); } }, 5000); const cleanup = () => { clearInterval(checkInterval); if (connsRef.current[conn.peer]) { delete connsRef.current[conn.peer]; setConns({ ...connsRef.current }); setMessages(prev => [...prev, { system: true, text: "誰かが退室しました" }]); } }; // 中略... conn.on('data', (data: { type: string; sender?: string; text?: string, ids?: string[] }) => { // 中略... } else if (data.type === 'leave') { // 明示的な退出通知を受け取った場合 cleanup(); conn.close(); } }); conn.on('close', cleanup); conn.on('error', cleanup); タブを閉じる時に明示的に切断するハンドラを定義したuseEffectと、setupConnection関数の最初に接続を定期的にチェックするヘルスチェック処理とcleanup関数を追加しました。 ...

2026年1月4日 · にあえん

React + Peerjsでグループチャットを実装してみる【1】

こんにちは。ナナオです。 今回はReact + peer.jsを使ってグループチャットを実装しようと思います。 peer.jsとは? Peer.jsはWebRTCを使ってP2Pを実現するためのツールです。 P2Pとは、サーバーを介さずにクライアント同士で直接データやり取りを行う方法のことです。 WebRTCは とりあえず実装したもの 最初に作ったグループチャットページの実装は以下の通りです。 タイトルの通り、React + peer.jsと、CSSにはtailwindを使用しています。 また、グループの部屋IDはURLのハッシュから取得するようにしています。 グループは、グループの発行者とそれ以外のメンバーで構成されます。 (実装はほぼGeminiがやってくれました。便利すぎ。) import { useState, useEffect, useRef } from 'react'; import Peer from 'peerjs'; import type { DataConnection } from 'peerjs'; const ChatApp = () => { const [myId, setMyId] = useState(''); const [messages, setMessages] = useState<{ sender?: string; text: string; system?: boolean }[]>([]); const [messageInput, setMessageInput] = useState(''); const [conns, setConns] = useState<{ [key: string]: DataConnection }>({}); // 接続中の全Peerを管理 {peerId: connection} const [copyStatus, setCopyStatus] = useState('URLをコピー'); const peerRef = useRef<Peer>(null); const scrollRef = useRef<HTMLDivElement>(null); // 初期化処理 useEffect(() => { const peer = new Peer(); peerRef.current = peer; peer.on('open', (id) => { setMyId(id); // URLのハッシュ(#)からIDを取得 const targetId = window.location.hash.substring(1); // #を除いた文字列を取得 if (targetId) { connectToPeer(targetId); } }); peer.on('connection', (conn) => { setupConnection(conn); }); return () => peer.destroy(); }, []); // 常に最新メッセージまでスクロール useEffect(() => { if (scrollRef.current) { scrollRef.current.scrollTop = scrollRef.current.scrollHeight; } }, [messages]); const setupConnection = (conn: DataConnection) => { conn.on('open', () => { setConns(prev => ({ ...prev, [conn.peer]: conn })); setMessages(prev => [...prev, { system: true, text: `${conn.peer.substring(0,5)}... が入室しました` }]); }); conn.on('data', (data) => { if (typeof data === 'string') { // 複数人でメッセージやり取りをできるように setMessages(prev => [...prev, { sender: conn.peer.substring(0,5), text: data }]); } }); conn.on('close', () => { setMessages(prev => [...prev, { system: true, text: '誰かが退室しました' }]); setConns(prev => { const newConns = { ...prev }; delete newConns[conn.peer]; return newConns; }); }); }; const connectToPeer = (id: string) => { if (conns[id] || !peerRef.current) return; // 接続済みならスキップ const conn = peerRef.current.connect(id); setupConnection(conn); }; const sendMessage = (e) => { e.preventDefault(); // 接続している全員に送信(ブロードキャスト) Object.values(conns).forEach(conn => { if (conn.open) { conn.send(messageInput); } }); setMessages(prev => [...prev, { sender: '自分', text: messageInput }]); setMessageInput(''); }; // 招待用URLをコピーする関数 const copyInviteLink = () => { const inviteUrl = `${window.location.origin}${window.location.pathname}#${myId}`; navigator.clipboard.writeText(inviteUrl).then(() => { setCopyStatus('コピー完了!'); setTimeout(() => setCopyStatus('URLをコピー'), 2000); }); }; return ( <div className="flex flex-col h-screen bg-slate-900 p-4 text-slate-100"> <div className="max-w-2xl mx-auto w-full flex flex-col h-full bg-slate-800 rounded-xl shadow-2xl overflow-hidden"> {/* ヘッダーセクション */} <div className="bg-slate-800 p-5 text-white"> <h1 className="text-xl font-bold mb-2 text-center text-indigo-400">P2P Chat</h1> <p className="text-[10px] text-slate-400">参加人数: {Object.keys(conns).length + 1}名</p> <div className="bg-slate-700 rounded-lg p-3 flex flex-col gap-2"> <div className="text-[10px] uppercase tracking-wider text-slate-400 font-semibold">Your ID</div> <div className="text-sm font-mono break-all text-indigo-200">{myId || '発行中...'}</div> {myId && ( <button onClick={copyInviteLink} className="mt-1 w-full bg-indigo-600 hover:bg-indigo-500 text-white text-xs py-2 rounded transition-colors font-bold" > {copyStatus} </button> )} </div> </div> {/* チャットログ */} <div ref={scrollRef} className="flex-1 overflow-y-auto p-4 space-y-4 bg-slate-900/50"> {messages.map((msg, i) => ( <div key={i} className={`flex flex-col ${msg.sender === '自分' ? 'items-end' : 'items-start'}`}> {msg.system ? ( <span className="text-[10px] text-slate-500 italic mx-auto my-2 uppercase tracking-widest">{msg.text}</span> ) : ( <> <span className="text-[10px] text-slate-400 mb-1 ml-1">{msg.sender}</span> <div className={`px-4 py-2 rounded-2xl text-sm max-w-[80%] break-all ${ msg.sender === '自分' ? 'bg-indigo-600 text-white rounded-tr-none' : 'bg-slate-700 text-slate-100 rounded-tl-none border border-slate-600' }`}> {msg.text} </div> </> )} </div> ))} </div> {/* 入力エリア */} <form onSubmit={sendMessage} className="p-4 bg-slate-800 border-t border-slate-700 flex gap-2"> <input type="text" className="flex-1 bg-slate-700 border-none rounded-lg px-4 py-3 text-sm text-white focus:ring-2 focus:ring-indigo-500 outline-none" placeholder="全員にメッセージを送信..." value={messageInput} onChange={(e) => setMessageInput(e.target.value)} /> <button type="submit" className="bg-indigo-600 p-3 rounded-lg hover:bg-indigo-500 transition-colors"> <svg xmlns="http://www.w3.org/2000/svg" className="h-5 w-5 fill-current" viewBox="0 0 20 20"> <path d="M10.894 2.553a1 1 0 00-1.788 0l-7 14a1 1 0 001.169 1.409l5-1.429A1 1 0 009 15.571V11a1 1 0 112 0v4.571a1 1 0 00.725.962l5 1.428a1 1 0 001.17-1.408l-7-14z" /> </svg> </button> </form> </div> </div> ); }; export default ChatApp; ポイントとしては以下の通りです。 ...

2026年1月3日 · にあえん

Syncthingのフォルダパスを変更する

こんにちは。ナナオです。 先日Syncthingについておススメする記事を書いたのですが、よくあることとして共有フォルダを設定した後でパスを変更したいケースがあります。 どのように対処すればよいか、備忘録として記録しておきます。 共有フォルダの移動方法 結論として、公式では以下のように記載されています。 FAQ — Syncthing documentation **Syncthingにはこれを直接行う方法がありません。**注意を怠ると危険を伴うためです。移動中に問題が発生し、他のデバイスに同期されるとデータ損失につながる可能性があります。 ローカルシステム上で同期フォルダをリネームまたは移動する簡単な方法は、Syncthing UIでフォルダを削除し、ディスク上で移動した後、新しいパスで再追加することです。 この操作は、フォルダが既にデバイス間で同期されている状態で行うことが重要です。そうしないと、移動後にどの変更が「優先されるか」が予測不能になります。他のデバイスで行われた変更が上書きされる可能性や、ローカルで行った変更が他のデバイスからの変更で上書きされる可能性があります。 別の方法として、Syncthingを停止し、ディスク上でフォルダ(.stfolderマーカーを含む)を移動した後、設定フォルダ内のconfig.xmlでパスを直接編集し(Syncthingの設定を参照)、Syncthingを再起動する方法があります。 ということで、UI上で一度フォルダを削除する必要があるようです。 やっていきましょう。 まず移動したいフォルダの編集を押します。 除去とはいを押します。 すると一覧からフォルダが消えます。 ただソースの実態は残っているので、別で削除しましょう。 削除したら新たにフォルダを設定します。 設定方法は昨日書いた記事で書いています。 除去されていないデバイスで一度共有設定を切って、再度共有します。 すると、先ほどフォルダを除去したデバイスで新規フォルダーとして検知されます。 これを新しいパスで保存して終わりです。 感想 やろうと思えばできるけど、ちょっと面倒なので最初からフォルダパスは慎重に決めるのがいいですね。。

2026年1月2日 · にあえん