~/ziphub — zsh
ZipHub·~/catalog~/articles~/feed~/sellers~/hire
auth login
[article]category · network

5つのVPSからProxmox搭載の1台の専用サーバーへ移行した話

@dignezzz · author17 min read2026-05-05free

TL;DR: TL;DR: 3社から5つのVPSに分散していたインフラ → Ryzen 7 9700X / 64GB ECC / 2× NVMe ZFSミラー搭載の専用サーバー、その上にProxmox VE。内部では、役割ごとにVM/LXC、ポートフォワーディングの代わりにNetBirdメッシュ、ブロッカー回避とTLS終端のための別VPSに出力ノード、バックアップにはPBS。コストはプラス、コントロールはプラス、精神的安定もプラス。この記事では、なぜ、どのように移行したのか、そしてその過程で踏んだ地雷について、皆さんが踏まないように解説します。

5つのVPSからProxmox搭載の1台の専用サーバーへ移行し、ホスティング業者への支払いをやめた話

TL;DR: 3社から5つのVPSに分散していたインフラ → Ryzen 7 9700X / 64GB ECC / 2× NVMe ZFSミラー搭載の専用サーバー、その上にProxmox VE。内部では、役割ごとにVM/LXC、ポートフォワーディングの代わりにNetBirdメッシュ、ブロッカー回避とTLS終端のための別VPSに出力ノード、バックアップにはPBS。コストはプラス、コントロールはプラス、精神的安定もプラス。この記事では、なぜ、どのように移行したのか、そしてその過程で踏んだ地雷について、皆さんが踏まないように解説します。

なぜこのプロジェクトを始めたのか

去年の年末、私は3つのホスティング業者から5つの異なるVPSに料金を支払っていることに気づきました。1つはDiscourseフォーラム用、2つ目はPHPとMySQLのレガシーフォーラム用、3つ目は請求システムとTelegramボット用、4つ目はVPN/プロキシ用、5つ目は静的サイトと各種小規模ツール用。さらに、n8nやVaultwarden、そしてお互いを監視し合っていた(他にどうしろと?)Uptime Kumaがいくつか動いていました。

問題点:

  • コスト。 合計すると、同等のスペックのしっかりした専用サーバー1台の価格とほぼ同額でした(ただし、VPSは共有、こちらは専有)。
  • 分離性の欠如。 あるクライアントのボットがメモリリークを起こすと、私のフォーラムが遅くなります。同じサーバー上で。最悪です。
  • バックアップが雑。 各VPSにS3へのrsync cronジョブがあり、保持期間もバラバラで、増分バックアップもなく、復旧は冒険でした。
  • ネットワークがカオス。 Cloudflare Tunnels経由でプロキシされたSSH、WireGuardを2つのVPS間に手動で設定、設定ファイルは3箇所に散らばっている。新しいマシンを追加するたびに、統合に1日かかりました。
  • アップデート・アポカリプス。 異なるバージョンとパッケージを持つ5つの異なるUbuntu/Debianで、アップデートは手動で行う必要があり、同時に行うことはできませんでした。半年に一度、私は「すべてをアップデートして、何も壊さない」というクエストを英雄的にクリアしていました。
  • CPUとRAMの遊休。 各VPSは「ピーク時」に合わせて予約されていますが、平均使用率は15〜20%です。お金が空中に飛んでいきます。

要するに、インフラが「ああ、もう一つサービスが必要だ、VPSを買おう」という原則で成長した典型的なケースです。これを統合する必要がありました。

選択したものと理由

ハードウェア

専用サーバーを選択しました:

  • CPU: AMD Ryzen 7 9700X (8コア / 16スレッド, Zen 5)
  • RAM: 64 GB DDR5 ECC 5200 MHz
  • ディスク: 2× NVMe 512 GB enterprise → ZFSミラー
  • ネットワーク: 1 Gbps 無制限
  • 価格: 同じOVHの平均的なVPS2台分程度

ハイパーバイザー

Proxmox VE 9.x。代替案も検討しましたが、簡単に言うと:

  • ベアメタルDocker — 分離性なし、正規のスナップショットなし、バックアップはcomposeの機能のみ、ディスクは単一のゴミ箱。
  • VMware ESXi — もはや無料ではなく、Broadcomのライセンスは素晴らしい、ありがとう。
  • XCP-ng — まあまあですが、個人的にはProxmoxを熟知しており、コミュニティもより活発です。
  • Kubernetes — いいえ、私は一人で、店に行くために飛行機を組み立てる必要はありません。

Proxmoxは必要なものすべてを提供してくれます:分離が必要なものすべて(Docker、特定のカーネル、異なるディストリビューション)のためのKVM仮想マシン。リソースを節約できる場所(DB、監視、小規模サービス)のためのLXCコンテナ。バックアップ用の組み込みPBS。ZFSは標準装備。スナップショット。ライブマイグレーション(もし将来2台目の専用サーバーができたら)。ターミナルで入力したくないときにマウスでクリックできるWebインターフェース。

アーキテクチャ: 全体図

まず完成図を示し、次にどのように到達したかを説明します。

ロジックは次のとおりです:

  1. 専用サーバー上にProxmoxが動作します。パブリックIP Y.Y.Y.Yvmbr0 に持ちます。
  2. 内部に2つ目のブリッジ vmbr1 を作成し、プライベートネットワーク 10.10.10.0/24 を設定します。これは「内部LAN」のアナログです — すべてのVMとLXCはここに接続され、ホストからのNAT経由で外部に出ます。それらにパブリックポートへの着信接続はありません。
  3. ホスト自体では、Caddy(内部リバースプロキシ)、nftables(ファイアウォール)、NetBird(VPNエージェント)の3つだけが動作します。ホスト上にはDockerやアプリケーションロジックはありません。ホストは神聖な牛であり、その仕事はハイパーバイザーとして機能し、ダウンしないことです。
  4. 仮想マシンは役割ごとに分割されます:1つはフォーラム用、1つはバックグラウンドサービスとボット用、1つはDokployとユーザーアプリケーション用、など。VM間は 10.10.10.x で直接通信できますが、外部へはホストのCaddy経由でのみ通信できます。
  5. LXCコンテナ — 「軽量」なものすべてに使われます:複数のバージョンのPostgresインスタンス、Proxmox Backup Server、監視ダッシュボード。これらも vmbr1 に接続されます。
  6. 個別の出力ノード(「Peer Caddy」)—これは別のプロバイダーの小規模VPSで、私のオーディエンスにとって適切なネットワークにあります。ここにCaddyをインストールし、TLS(Let's Encrypt)を終端し、HTTPをNetBirdメッシュ経由で専用サーバー内部にプロキシします。すべてのパブリックドメインのDNSは、専用サーバーではなく、このVPSを参照します。
  7. NetBirdメッシュ — 専用サーバー、出力ノード、私のラップトップ(まだすべてを移行していない古いサーバーもいくつか)を接続します。これは、外部にポートを開放せずに、SSO認証上でのWireGuardです。

出力ノードを介したこのような分離が必要な理由については、後で詳しく説明します。

Proxmoxのインストール: どのようにセットアップしたか

専用サーバーを注文し、最初の起動時にProxmoxをレスキューモード/KVM経由でインストールしました(OVHには両方あります — KVMの方がプロセス全体が見えるので便利です)。インストール時:

  • ZFS RAID1 を両方のNVMeに設定 (ashift=12compression=lz4 をすぐに有効化)
  • デフォルトのパーティショニング。Proxmoxが rpool/ROOTrpool/data、swapを自動作成します。

その後、インストール後の基本的なクリーンアップを行います。これは必須です。これをしないと、Proxmoxはサブスクリプションエラーを出し、最適に動作しません。

bash
1# 1. enterpriseリポジトリを削除(サブスクリプションなし)
2sed -i 's/^deb/#deb/' /etc/apt/sources.list.d/pve-enterprise.list
3echo "deb http://download.proxmox.com/debian/pve bookworm pve-no-subscription" \
4  > /etc/apt/sources.list.d/pve-no-subscription.list
5apt update && apt full-upgrade -y
6
7# 2. "No valid subscription" の警告ウィンドウを削除
8sed -Ezi.bak "s/(Ext\.Msg\.show\(\{[^}]+?title: gettext\('No valid sub)/void\(\{ \/\/\1/g" \
9  /usr/share/javascript/proxmox-widget-toolkit/proxmoxlib.js
10systemctl restart pveproxy
11
12# 3. ホスト名/タイムゾーン
13hostnamectl set-hostname dedik-waw
14timedatectl set-timezone Europe/Warsaw

SSHハードニング

すぐに。後で「やる」のではなく。後では決して来ない。

bash
1cat > /etc/ssh/sshd_config.d/hardening.conf << 'EOF'
2Port 22222
3PermitRootLogin prohibit-password
4PasswordAuthentication no
5MaxAuthTries 3
6ClientAliveInterval 300
7ClientAliveCountMax 2
8EOF
9
10mkdir -p ~/.ssh
11echo "ssh-ed25519 AAAA... your-key" >> ~/.ssh/authorized_keys
12chmod 600 ~/.ssh/authorized_keys
13systemctl restart sshd

ポート22はファイアウォールで閉じ、非標準ポートのみを開放しました。これは隠蔽によるセキュリティではなく、ログのバックグラウンドノイズを低減するためです。ポート22には、最初の1時間で50,000件のブルートフォース試行が届きますが、ログは読めません。ポート22222では、静かで異常をリアルに監視できます。

ZFS — NVMeと64GB RAM向け設定

ZFSはデフォルトでARCのためにRAMの半分を消費しようとします。これはファイルサーバーには正常ですが、ハイパーバイザーには全く不向きで、メモリは仮想マシンに必要です。ARCを合理的な16GBに制限します:

bash
1cat > /etc/modprobe.d/zfs.conf << 'EOF'
2options zfs zfs_arc_max=17179869184
3options zfs zfs_arc_min=4294967296
4EOF
5update-initramfs -u

私のメモリ配分:

  • 約16GB — ZFS ARC
  • 約44GB — VM/LXC
  • 約4GB — システム、バッファ、オーバーヘッド

プール自体の便利な設定:

bash
1zfs set compression=lz4 rpool
2zfs set atime=off rpool
3zfs set xattr=sa rpool
4zfs set dnodesize=auto rpool
5zfs set relatime=on rpool
6
7zfs set recordsize=64K rpool/data    # VMディスク用
8zfs set sync=standard rpool/data
9
10zpool set autotrim=on rpool          # NVMeに不可欠

compression=lz4 は、CPU負荷をほとんどかけずに、スペースを20〜30%節約します。VMのブロックデバイス用のrecordsize=64Kは、パフォーマンスと書き込み増幅の間の最適な妥協点です。

内部ネットワークとファイアウォール

NATで外部に出るプライベート接続用に2つ目のブリッジを作成しました:

1auto vmbr1
2iface vmbr1 inet static
3    address 10.10.10.1/24
4    bridge-ports none
5    bridge-stp off
6    bridge-fd 0
7    post-up   echo 1 > /proc/sys/net/ipv4/ip_forward
8    post-up   iptables -t nat -A POSTROUTING -s 10.10.10.0/24 -o vmbr0 -j MASQUERADE
9    post-down iptables -t nat -D POSTROUTING -s 10.10.10.0/24 -o vmbr0 -j MASQUERADE

これにより、vmbr1 ブリッジ内の任意のVM/LXCはローカルアドレス 10.10.10.x を取得し、ホスト経由でインターネットにアクセスできますが、外部からは見えません。

nftablespolicy drop で設定します。必要なものだけを開放し、残りはサイレントにドロップします。最小限のセット:

nft
1table inet filter {
2    chain input {
3        type filter hook input priority 0; policy drop;
4
5        iif lo accept
6        ct state established,related accept
7        ip protocol icmp accept
8        ip6 nexthdr icmpv6 accept
9
10        tcp dport 22222 accept           # SSH
11        tcp dport 8006 accept            # Proxmox UI (NetBird経由のみ、下記参照)
12        tcp dport { 80, 443, 8080 } accept   # Caddy
13        udp dport 29899 accept           # NetBird
14
15        iif vmbr1 accept                 # 内部ネットワーク — すべて許可
16        iif wt0 accept                   # NetBirdインターフェース — 信頼済み
17    }
18
19    chain forward {
20        type filter hook forward priority 0; policy drop;
21        iif vmbr1 oif vmbr0 accept
22        ct state established,related accept
23    }
24}

Proxmox UI (ポート8006) へのアクセスは、CF/ファイアウォールでインターネットから物理的にブロックし、NetBird経由でのみアクセスします。パブリックIPではUIは全く応答しません。PBS UI、ダッシュボード、およびすべての管理インターフェースも同様に、メッシュ経由でのみアクセスします。外部に公開されているのは、実際に公開されているサービスポートのみです。

NetBirdメッシュ: なぜ標準のWireGuardではないのか

通常のWireGuardを長年使用していましたが、サーバーが3台を超えると管理が苦痛になりました。ピアを追加するたびに「すべてのノードで設定を更新し、allowed_ipsを忘れずに、デーモンを再起動し、他のピアへのルートを壊していないことを確認する」という作業になります。

NetBirdはWireGuard上の管理プレーンです。技術的には、内部では依然としてwgですが:

  • ノードはシグナルサーバーを介して互いを見つけます。NATトラバーサルは自動です。
  • SSO(私の場合はAuthentik)経由で認証され、キーの共有はありません。
  • アクセスはWeb UIでポリシーを介して管理され、マシンのグループとそれらの間のルールを作成できます。
  • 組み込みSSHプロキシ:ssh user@machine.netbird.cloud でSSO認証(Authentikでの承認付き)を受けられます。ノードで外部にポート22を開放する必要すらありません。

NetBirdエージェントをインストールしました:

  • Proxmoxホスト上
  • 管理者または他のVMからアクセスする必要のある各VM上
  • 出力ノード上
  • 私のラップトップ上

結果として、ポートフォワーディング、ファイアウォールの穴、追加のBastionホストなしで、1つのコマンドで任意のマシンにラップトップからアクセスできます。そして、プライベートVMにはSSHポートが開放されていません。それらはすべて vmbr1 に加えてメッシュの wt0 に接続されています。

VM vs コンテナ: 私なりの解決策

ほぼ常に機能する簡単なルール:

いつ何を使うか
DockerワークロードVM (LXC内のDockerは、特にoverlay2の場合、気まぐれになることがある)
Dockerを使わないデータベースLXC (軽量、高速、ZFS経由でディスクに直接アクセス可能)
システムサービス (PBS, 監視, 小規模ツール)LXC
独自のカーネル/ネットワークトリックが必要なアプリケーションVM
VPNノード (WG, AmneziaWG)LXC (オーバーヘッド最小限)

最終的に、私は以下のような構成になりました:

仮想マシン (KVM):

  • forum — 標準のランチャーコンテナ内のDiscourse。内部でDockerを使用。
  • services —Webhookボット、AIボット、請求パネル、Vaultwarden、Uptime Kumaなど、多数の小規模サービス。すべてDocker Compose内、サービスごとにフォルダ分け。
  • legacy — 古いPHPフォーラム(IPS)。ネイティブPHP-FPMとVM上のCaddyを使用。Dockerにパッケージ化するのは避けたかった — レガシーが多すぎ、そのままにしておく方が簡単でした。
  • dokploy — Docker Swarm + Dokploy(Next.js、Postgres、Redis用)、Git経由のデプロイ。

LXC:

  • pbs — Proxmox Backup Server (下記参照)。
  • pg16pg17pg18 — 3つの異なるメジャーバージョンのPostgres。理由: 異なるアプリケーションが異なるメジャーバージョンを要求し、それぞれをアップグレード・ダウングレードしたくない。
  • dashboards — Homarr (スタートページ)、Uptime Kuma、Beszel hubを含む1つのコンテナ。すべて同じCompose内、なぜならそれらは論理的に同じこと、つまり監視のためだからです。

各VMには、負荷に応じて静的IP 10.10.10.x (範囲別に設定、検索しやすいように)、2〜8 vCPUコア、2〜8 GB RAMが割り当てられます。コアは安全にオーバープロビジョニングできます — VM全体で物理コア数よりも多くのコアが割り当てられていますが、誰も同時にCPUに負荷をかけない限り、これはうまく機能します。

リバースプロキシ: 二段構えのCaddy

Proxmox内では、VMではなくホスト上にCaddyをインストールしました。なぜか?それは専用サーバーのパブリックポートをリッスンし、Hostヘッダーに基づいて適切なVMにルーティングする必要があるからです。それのためにポートフォワーディング付きの個別のVMを起動するのは、メリットのない余分なレイヤーです。Caddyは軽量で、Goで書かれており、systemdユニット、設定ファイルは1つです。

このホスト上のCaddyはHTTPポート8080のみをリッスンし、10.10.10.x のバックエンドにルーティングします。TLSは一切ありません。

caddy
1:8080 {
2    @forum host forum.example.com www.forum.example.com
3    handle @forum {
4        reverse_proxy http://10.10.10.111:80 {
5            header_up X-Forwarded-Proto https
6            header_up X-Real-IP {header.X-Real-IP}
7            header_up Host {host}
8        }
9    }
10
11    @panel host panel.example.com api.panel.example.com
12    handle @panel {
13        reverse_proxy http://10.10.10.111:8081 {
14            header_up X-Forwarded-Proto https
15            header_up X-Real-IP {header.X-Real-IP}
16            header_up Host {host}
17        }
18    }
19
20    @dokploy host app1.example.com app2.example.com
21    handle @dokploy {
22        reverse_proxy http://10.10.10.114:80 {
23            header_up X-Forwarded-Proto https
24            header_up X-Real-IP {header.X-Real-IP}
25            header_up Host {host}
26        }
27    }
28}

そして、TLS終端と証明書は、別の出力ノード(Docker内)で、同様の原則で処理します:

caddy
1{
2    email me@example.com
3}
4
5forum.example.com {
6    reverse_proxy http://100.x.y.z:8080 {
7        header_up X-Forwarded-Proto {scheme}
8        header_up X-Forwarded-For {remote_host}
9        header_up X-Real-IP {remote_host}
10        header_up Host {host}
11    }
12}

100.x.y.z は専用サーバーのNetBirdアドレスです。リクエストはHTTPSで出力ノードに到着し、復号化され、NetBirdメッシュ経由で専用サーバーにHTTPで送信されます。そこではProxmox CaddyがHostヘッダーでマッチングし、内部VMにルーティングします。出力ノードと専用サーバー間は、設計上HTTPです — メッシュは信頼できるため、ここには外部トラフィックはありません。

なぜ出力ノードが必要なのか

いくつかの理由があります:

  1. ブロッカー回避。 OVHのIPサブネットの一部はロシアでブロックされています。出力ノードは、ブロックされていないIPを持つプロバイダーの元にあります。パブリックドメインのDNSはそこを参照します。
  2. TLS終端を一元化。 証明書はそこでのみ発行され、Let's EncryptのチャレンジIPはそのIPのみになります。これにより、すべてのDNSクリアリングチェックが簡素化され、専用サーバーを80/443でインターネットから完全に不可視にすることができます。
  3. 追加の分離レイヤー。 DDoS攻撃を受けた場合、それは専用サーバーではなく、出力ノードが攻撃されます。出力ノードには最小限のデータしかなく、再作成は10分で完了します。
  4. パブリックフロントエンドとバックエンドの分離。 専用サーバー上でCaddyのアップデートや実験を安全に行うことができます。インターネットへの実際のインターフェースは別途、安定しています。

唯一の欠点は、追加のホップにより各リクエストに約5〜15ミリ秒の遅延が発生することです。Webではほとんど感知できません。

また、一部のサービスでは、出力ノードを介さずにプロキシしたいドメイン(ステータスエンドポイント、監視、DNSがすでに専用サーバーを参照しているサービスなど)のために、ホスト上のポート80/443で直接Caddyを実行しています。これは並行して動作します。ホスト上のCaddyは :8080 (出力ノードからの内部トラフィック) と :80/:443 (インターネットからの直接トラフィック) の両方をリッスンし、ルーティングは異なります。

マイグレーション: 涙なく、ほぼダウンタイムなしで移行した方法

これは最も恐ろしい段階でした。5つのサーバー、約30のサービス、アクティブなユーザーがいる本番フォーラム、失われることのできないアクティブなサブスクリプションを持つ請求システム、データベース。

戦略はサービスごとに1つずつ移行し、「最も重要でないものから最も重要なものへ」の順序です:

  1. まず、小規模ツールと静的サイトを移行しました(もし何か問題があっても、誰も気づかないでしょう)。
  2. 次に、ボットとバックグラウンドサービス(これも1分程度のダウンタイムは許容できます)。
  3. 次に、データベース(事前に復旧テスト済み)。
  4. 次に、これらのデータベースを参照するアプリケーション。
  5. 最後に、メインフォーラム。最終同期ラウンドとDNS切り替えを実施。

マイグレーションチャネル

まず、古いサーバーにNetBirdをセットアップしました。これにより、2つの問題が即座に解決されました。内部ネットワークに安全にSSHでアクセスでき、rsyncはNetBird経由のWireGuardで実行され、データをオープンインターネットに公開しません。

各サービスに対するコマンドは、ほぼ以下のようでした:

bash
1# 新しい専用サーバーから、古いサーバーのNetBird IP経由で:
2rsync -avzP --delete \
3  -e "ssh -p 5322" \
4  root@100.76.108.210:/var/discourse/shared/standalone/ \
5  /target/discourse/shared/standalone/

データベースについては、ファイルコピーではなくダンプとリストアを行います。Postgres/MySQLのファイルを「ホット」な状態でコピーしないでください。それは死への切符です。

bash
1# 古いサーバーで
2docker exec -it shm-vsem-mysql mysqldump -u root -p shm-vsem | \
3  gzip > /tmp/shm-vsem.sql.gz
4
5# 新しいサーバーで、NetBird経由
6scp -P 5322 root@100.76.108.210:/tmp/shm-vsem.sql.gz /tmp/
7zcat /tmp/shm-vsem.sql.gz | docker exec -i shm-vsem-mysql mysql -u root -p shm-vsem

Discourseについては、ネイティブのdiscourse backup / discourse restore./launcher enter app 経由で使用します。内部で、Postgresダンプ、アップロード、設定ファイルを適切に収集し、同様に復元します。

DNS切り替え

新しい場所でサービスを起動し、内部アドレスで動作することを確認した後、5〜15分間両方のインストールを並行して実行し、データの同期を確認してからDNSを新しいサーバーに切り替えました。DNSレコードのTTLは、切り替えの1日前に60〜300秒に事前に下げておきました。

アクティブなユーザーが常に投稿しているDiscourseの場合、次のようにしました:

  1. 新しい場所ですべてをセットアップしました。
  2. 検証(ログイン、投稿、S3へのファイルアップロード、検索)を実行しました。
  3. 古いインストールでread_only_modeを有効にしました(Discourseはこれを標準でサポートしています)。
  4. 最終的なuploadsのrsyncと最終的なDBダンプを実行しました。
  5. 新しいサーバーで起動し、read-onlyを無効にしました。
  6. DNSを切り替えました。
  7. 古いサーバーには1週間何も触りませんでした — 万が一のためです。

ユーザーにとって実際のダウンタイムは、サービスあたり約2分でした。

途中で発生した驚き

  • pct restore時のパーミッション。 unprivileged 1 のLXCはUIDを100000シフトしてマッピングします。古いサーバーでUID 1000のファイルがあった場合、新しいサーバーではUID 101000になり、アプリケーションはそれを見つけられません。これは、リストア後のchown、またはリスクを理解している場合は--unprivileged 0で解決します。
  • OVH VM内のDocker ipv6 が時々正しく起動しないことがありました — Dockerデーモンでipv6を無効にすることで解決しました ("ipv6": false)。
  • VMの時間。 ホストとVM間のシステム時間のずれが何度か発生し、TLSと署名が壊れました。解決策は、各VMでのsystemd-timesyncdまたはchrony、およびProxmoxエージェントでのhost.use-timeです。
  • Discourseのバージョン間マイグレーション。 古いサーバーのDiscourseが、新しくインストールしたものよりも新しい場合、restoreは失敗します。新しいインストールでは、バージョンは同じかそれ以上である必要があります。
  • Dokployのイメージレジストリ。 Dokployのメタデータをdokploy-postgresダンプで移行したとき、DBには古いローカルレジストリへのリンクが残っていました。サービスが起動しようとすると 100.76.117.115:5000 にアクセスして No such image エラーで失敗しました。解決策は、Dokploy UIで各アプリケーションを再ビルドすることです。ローカルビルドはイメージを新しいサーバーに配置します。

バックアップ: 同じ専用サーバー内のPBS + オフサイト

ここで、自分自身との哲学的な議論がありました:同じハードウェアにバックアップするかしないか。答え:はい、そしていいえ。

レベル1 — プール自体のZFSスナップショット。 これらはほぼ無料(コピーオンライト)で、瞬時に作成、瞬時に復元できます。私は rpool/data の自動スナップショットを15分間隔で24時間保持し、さらに毎日1週間保持しています。zfs-auto-snapshotを使用しています。「あ、本番DBを削除してしまった」という状況からの保護:

bash
1apt install zfs-auto-snapshot
2# その後、cron経由でfrequent/hourly/daily/weekly/monthlyが自動作成されます。

レベル2 — 同じホスト上の別LXCコンテナにProxmox Backup Serverを配置。 これはvzdump経由のVM/LXCの完全な増分バックアップで、チャンクレベルで重複排除されます。PBSはZFSの別データセットにスナップショットを保存し、リポジトリとして認識します。各VMには独自の増分チェーンがあります。

Proxmoxでの設定: Datacenter → Backup → Add。スケジュール: 毎日午前4時、保持期間 keep-daily=7 keep-weekly=4 keep-monthly=3。これらはすべてマウス操作です。

同じホスト上のLXCコンテナにPBSを配置するのは妥協です。利点: 別途ハードウェアを用意する必要がなく、バックアップはlocalhost経由で、速度はローカルSSD並み、保持/重複排除は完璧に機能します。欠点: 専用サーバー全体が破損した場合、バックアップも失われます。そのため、以下のものがあります...

レベル3 — PBSリポジトリをオフサイトS3に同期。 PBSはS3互換ストレージへのsync jobをサポートしています。毎晩、SelectelのS3(Backblaze, Wasabi, Cloudflare R2などでも可)の別バケットにスナップショットをコピーします。保持期間は2週間です。それ以上は不要だからです。長期保存にはローカルPBSがあり、オフサイトは「データセンター全体が焼失した」という保険です。

レベル4 — アプリケーションデータは別途。 Discourse自体は独自のバックアップを作成し、S3に保存します(これはネイティブ機能です)。そのため、PBSや専用サーバー全体が失われたとしても、Discourseの整合性の取れたダンプはまだクラウドにあります。

bash
1# PBSからVM全体を復元 — 文字通り1コマンド:
2pvesm list backup-pbs    # 利用可能なものを確認
3qmrestore backup-pbs:backup/vzdump-qemu-201-2026_04_06-04_00_03.vma.zst 999 \
4  --storage local-zfs
5# 1分後、VMID 999に午前4時時点のforum-VMのコピーが起動します。

これは重要な点です:復元テストをしていないバックアップはバックアップではありません。月に一度、ランダムなVMを空のVMIDにテストリストアし、起動すること、アプリケーションが動作することを確認してから削除します。退屈ですが、一度これで、cronジョブが /tmp/... に書き込み、バックアップ時にスキップされ、リストア後にアプリケーションが手動ステップを要求することを発見しました。

モニタリング

15台のマシンがあるときに、Prometheus + Grafana + Alertmanager + Lokiのようなスイートを構築するのは好きではありません。そのため、3つの軽量ツールを1つのLXCコンテナにまとめました:

  • Beszel — 各VM(CPU、RAM、ディスク、ネットワークインターフェース、Dockerコンテナ)のエージェントからメトリクスを収集します。ハブはLXC内で動作し、エージェントはsystemdユニット経由で各VM/LXCで動作します。Beszelの認証はPocketBase経由ですが、時々不便です(下記の落とし穴参照)。
  • Uptime Kuma — HTTP/HTTPS/Ping/TCPによるプローブ。パブリック(ドメイン)、内部サービス(NetBird経由)、メッシュノードへのpingすべてを設定しています。アラートはTelegramに送信します。
  • Homarr — すべての管理インターフェースへのリンクを持つスタートページ。PBS UIがどのポートで動作しているか、Dokployがどのポートか、Vaultwardenがどのポートか覚える必要がありません。Homarrを開いてクリックするだけです。

すべて3つとも同じDocker Compose、同じLXCコンテナ内で動作し、NetBird経由でのみアクセス可能です。

Beszelの落とし穴、皆さんが踏まないように: BeszelはPocketBaseに2つのユーザーテーブルがあります — _superusers (CLI/API用) と users (Web UI用)。CLI superuser upsert でパスワードをリセットすると、_superusers のみがリセットされ、UIで users を使用してログインしてもパスワードが一致しません。これはREST API経由で /api/collections/users/records/<id> へのPATCHリクエストで解決できます。

最終的に得られたもの

約3週間のマイグレーション後、最後のサービスが落ち着いたときに、結果をまとめました:

パラメータ以前現在
サーバー/請求書3社で5台専用サーバー1台 + マイクロVPS1台
月額料金~$X~$X/2
空きリソース「まあ十分」8 vCPUと30GB RAMの余裕
バックアップ各VPSのS3へのrsync cronPBS + オフサイト + ZFSスナップショット
復旧「まあ、1日くらい」PBSからVMあたり2〜5分
SSHアクセス5つの異なるポートとキーSSO付きの1つのNetBirdメッシュ
サービス分離共有ゴミ箱各VM/LXCに分離
アップグレード前のスナップショット「祈る」qm snapshot 201 pre-update
ルーチンタスク用Web UIどのWeb UI?Proxmox UI

これにより得られた最大の感情的なメリットは、何かを触ることを恐れなくなったことです。アップグレード、マイグレーション、実験など、危険な操作はすべてスナップショットから始まり、30秒でコミットするかロールバックするかで終わります。エラーのコストが桁違いに下がったため、新しいことを試す頻度が増えました。

繰り返す場合のチェックリスト

もしあなたが私と同じ症状に悩んでいるなら、以下に移動すべき順序の短いチェックリストがあります:

  1. すべてのVPSの合計支払額を計算し、OVH/Hetzner/LeaseWebの専用サーバーの価格と比較してください。驚くでしょう。
  2. ZFSを使用する予定なら、ECC RAMを必ず選択してください。節約しないでください。
  3. 最低2台のディスクをミラーリングしてください。ディスク1台では本番環境にはなりえません。
  4. ProxmoxをKVM/IPMI経由でインストールしてください。rescue + debootstrapではありません。手間が省けます。
  5. すぐにSSHキー、非標準ポート、パスワード無効化、fail2banの設定を行ってください。
  6. すぐにZFS ARCを制限してください。デフォルトではRAMの半分を消費します。
  7. 2つ目のブリッジ vmbr1 を作成し、NAT付きのプライベートネットワークを設定してください。すべてのVM/LXCをそこに接続します。仮想マシンにはパブリックIPを割り当てないでください。
  8. マイグレーションを開始する前に、NetBird(またはTailscale、Headscale)をセットアップしてください。これがマイグレーションチャネルと管理者アクセスになります。
  9. Proxmox UIを外部に公開しないでください。 VPNメッシュ経由のみにしてください。
  10. 別のプロバイダーの小さな出力VPS1台をTLS終端とブロッカー回避用に用意するのは必須ではありませんが、非常に便利です。コストはわずかで、メリットは大きいです。
  11. PBSを別LXCコンテナに入れ、オフサイトS3に同期してください。バックアップは自動化されるべきです。
  12. 月に一度はテストリストアを実行してください。そうでないと、バックアップではなく自己満足のためのファイルになります。
  13. サービスごとに1つずつ、重要度の昇順でマイグレーションしてください。「週末にすべてを移動させよう」とはしないでください。
  14. DNSのTTLは、切り替えの1時間前ではなく、1日前に下げてください。
  15. 各サービスについて、ドキュメントを作成してください:設定ファイルはどこか、起動方法、バックアップ方法、復旧方法。半年後に自分自身に感謝するでしょう。

まだ完了していないこと

完全性のため:私のマイグレーションはまだ100%完了していません。残りは以下の通りです:

  • 古いサーバーからのRemnawaveパネル数件(来週末に予定)。
  • カスタムビルドのCaddyプラグイン(一時的に動作中)。
  • 古いCrowdSec — 新規に別個にインストールし、移行しません。

しかし、すべてクリティカルなもの(フォーラム、請求システム、ボット、データベース)はすでに専用サーバー上にあり、安定して動作し、バックアップと監視が行われています。

特定のステップ、設定、または私が踏んだ落とし穴について質問があれば、コメントで質問してください。詳しく説明します。特に、他のハイパーバイザー(XCP-ng、ESXi、またはベアメタルDocker Swarm)で同様の問題をどのように解決しているか聞きたいです — もしかしたら、私が何かを見落としているかもしれません。

皆さんのインフラ統合の成功を祈っています。請求書の削減、コントロールの向上 — それだけの価値はあります。

~17 min read · scroll to continue ↓

## discussion

$ topics --entity=article0
sign in to start or join a discussion
No discussions yet — start one to break the ice.
↑↓ nav open⌘K palettei install? help