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

Proxmox가 설치된 단일 전용 서버로 5개의 VPS에서 이전한 이야기

@dignezzz · author17 min read2026-05-05free

TL;DR: TL;DR: 3개 제공업체의 5개 VPS로 분산된 인프라 → Proxmox VE 기반의 단일 전용 서버 Ryzen 7 9700X / 64GB ECC / 2× NVMe ZFS-mirror. 내부에는 역할별 VM/LXC, 포트 전달 대신 NetBird 메쉬, 차단 우회 및 TLS 종료를 위한 별도 VPS의 출력 노드, 백업을 위한 PBS. 금전적으로 이득, 통제력 증가, 신경 안정. 이 글에서는 왜, 어떻게, 그리고 길을 가면서 어떤 함정에 빠졌는지 알려주어 여러분은 그러지 않도록 합니다.

Proxmox가 설치된 단일 전용 서버로 5개의 VPS에서 이전하고 호스팅 업체에 돈을 더 이상 지불하지 않기로 결정한 이야기

TL;DR: 3개 제공업체의 5개 VPS로 분산된 인프라 → Proxmox VE 기반의 단일 전용 서버 Ryzen 7 9700X / 64GB ECC / 2× NVMe ZFS-mirror. 내부에는 역할별 VM/LXC, 포트 전달 대신 NetBird 메쉬, 차단 우회 및 TLS 종료를 위한 별도 VPS의 출력 노드, 백업을 위한 PBS. 금전적으로 이득, 통제력 증가, 신경 안정. 이 글에서는 왜, 어떻게, 그리고 길을 가면서 어떤 함정에 빠졌는지 알려주어 여러분은 그러지 않도록 합니다.

왜 이런 일을 시작했는가

작년 말, 나는 3개 호스팅 업체의 5개 서로 다른 VPS에 비용을 지불하고 있다는 것을 깨달았습니다. 하나는 Discourse 포럼용, 두 번째는 PHP와 MySQL 기반 레거시 포럼용, 세 번째는 빌링 및 Telegram 봇용, 네 번째는 VPN/프록시용, 다섯 번째는 정적 파일 및 기타 자잘한 도구용이었습니다. 또한 n8n과 Vaultwarden, 서로를 순환적으로 모니터링하는 Uptime Kuma 몇 개도 거기서 돌아가고 있었습니다 (다른 방법이 있겠습니까?).

문제점:

  • 비용. 총 비용이 이러한 사양을 갖춘 괜찮은 전용 서버 하나의 비용과 거의 같았습니다 (하지만 공유가 아닌 전용이었습니다).
  • 격리 부족. 한 클라이언트 봇의 메모리 누수가 발생하면 포럼이 느려집니다. 같은 서버에서요. 정말 멋지죠.
  • 백업 — 엉망진창. 각 VPS마다 S3로 자체 rsync 크론 작업이 설정되어 있었고, 보존 기간도 다르고, 증분 백업도 없고, 복원은 완전한 모험이었습니다.
  • 엉망진창인 네트워크. Cloudflare Tunnels를 통해 전달된 다른 포트로 SSH 접속, 수동으로 두 개의 VPS 사이에 설정한 WireGuard, 세 곳에 분산된 구성 파일. 새 머신을 추가할 때마다 통합에 하루가 걸렸습니다.
  • 업데이트 재앙. 서로 다른 버전과 패키지를 가진 5개의 서로 다른 Ubuntu/Debian에서 업그레이드를 수동으로 해야 했고, 동시에 할 수 없었습니다. 6개월마다 "모든 것을 업데이트하고 아무것도 망가뜨리지 않기"라는 퀘스트를 영웅적으로 통과했습니다.
  • 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의 중간급 VPS 두 개 가격 정도

하이퍼바이저

Proxmox VE 9.x. 대안도 고려했지만 짧게 요약하면:

  • 기본 Docker — 격리 기능 없음, 정상적인 스냅샷 없음, compose로만 백업 가능, 디스크 — 단일 쓰레기통.
  • VMware ESXi — 더 이상 무료가 아님, Broadcom 라이선스 훌륭함, 감사합니다.
  • XCP-ng — 괜찮지만 개인적으로 Proxmox를 아주 잘 알고 있고 커뮤니티가 더 활발합니다.
  • Kubernetes — 아니요, 저는 혼자이고, 가게에 가기 위해 비행기를 조립할 필요는 없습니다.

Proxmox는 제가 필요한 모든 것을 제공합니다: 격리가 필요한 모든 것(Docker, 특정 커널, 다양한 배포판)을 위한 KVM 가상 머신; 리소스를 절약할 수 있는 곳(DB, 모니터링, 작은 서비스)을 위한 LXC 컨테이너; 백업을 위한 내장 PBS; 기본 ZFS; 스냅샷; 라이브 마이그레이션(나중에 두 번째 데딕이 생긴다면); 터미널에 입력하기 싫을 때 마우스로 클릭할 수 있는 웹 인터페이스.

아키텍처: 전체 구성도

먼저 결과물을 보여드리고, 어떻게 도달했는지 설명하겠습니다.

로직은 이렇습니다:

  1. 전용 서버에는 Proxmox가 실행됩니다. vmbr0에 공개 IP Y.Y.Y.Y를 가지고 있습니다.
  2. 내부적으로 **두 번째 브리지 vmbr1**을 생성했으며, 비공개 네트워크 10.10.10.0/24를 사용합니다. 이는 "내부 LAN"의 아날로그입니다. 모든 VM 및 LXC는 여기에 연결되어 있으며, 호스트의 NAT를 통해 외부로 통신하고, 직접적인 공개 포트는 없습니다.
  3. 호스트 자체에는 세 가지 것만 실행됩니다: Caddy (내부 리버스 프록시), nftables (방화벽) 및 NetBird (VPN 에이전트). 호스트에는 Docker가 없고, 호스트에는 애플리케이션 로직이 없습니다. 호스트는 신성한 소이며, 하이퍼바이저 역할을 하고 다운되지 않는 것이 임무입니다.
  4. 가상 머신은 역할별로 분리됩니다: 하나는 포럼용, 하나는 백그라운드 서비스 및 봇용, 하나는 사용자 애플리케이션용 Dokploy, 기타 등등. 이들 간에는 10.10.10.x를 통해 직접 통신이 가능하며, 외부로는 호스트의 Caddy를 통해서만 나갑니다.
  5. LXC 컨테이너는 "가벼운" 모든 것(여러 버전의 Postgres 인스턴스, Proxmox 백업 서버, 모니터링 대시보드)을 위한 것입니다. 이들도 vmbr1에 있습니다.
  6. 별도의 출력 노드 ("Peer Caddy")는 다른 제공업체의 작은 VPS로, 제 대상 고객에게 적합한 네트워크에 있습니다. 여기에는 Caddy가 설치되어 TLS(Let's Encrypt)를 종료하고 NetBird 메쉬를 통해 전용 서버 내부로 HTTP를 프록시합니다. 모든 공개 도메인의 DNS는 이 VPS를 가리키지, 전용 서버를 가리키지 않습니다.
  7. NetBird 메쉬는 전용 서버, 출력 노드 및 제 노트북(아직 모든 것을 이전하지 않은 이전 서버 몇 개 포함)을 연결합니다. 이는 SSO 인증을 통한 WireGuard이며, 외부 포트 개방이 없습니다.

출력 노드를 통한 이러한 격리가 필요한 이유—나중에 다시 설명하겠습니다.

Proxmox 설치: 어떻게 설정했는가

전용 서버를 주문하고, 처음 부팅 시 rescue 모드/KVM을 통해 Proxmox를 설치했습니다 (OVH에는 둘 다 있습니다. KVM이 더 편리했고, 전체 프로세스를 볼 수 있었습니다). 설치 시:

  • ZFS RAID1 (두 NVMe 모두에 대해 ashift=12, compression=lz4 즉시 활성화)
  • 기본 파티션 설정, Proxmox는 rpool/ROOT, rpool/data, swap을 자동으로 생성합니다.

설치 후 기본 정리 작업. 필수입니다. 그렇지 않으면 Proxmox가 구독 오류를 표시하고 최적으로 작동하지 않습니다.

bash
1# 1. enterprise repo 제거 (구독 없음)
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/proxmromoxlib.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에서는 첫 시간 안에 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

제 메모리 할당:

  • ~16 GB — ZFS ARC
  • ~44 GB — VM/LXC
  • ~4 GB — 시스템, 버퍼, 오버헤드

풀 자체의 유용한 설정:

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를 위한 두 번째 브리지를 만들었습니다:

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, 대시보드 및 모든 관리용 UI도 마찬가지입니다. — 메쉬를 통해서만 접근합니다. 외부에는 실제 공개 서비스 포트만 열려 있습니다.

NetBird 메쉬: 일반 WireGuard를 사용하지 않은 이유

일반 WireGuard를 오랫동안 사용했는데, 서버가 세 개 이상이 될 때까지는 괜찮았습니다. 그 후에는 구성 파일을 관리하는 것이 고통스러웠습니다: 각 피어를 추가하는 것은 "모든 노드의 구성을 업데이트하고, allowed_ips를 잊지 말고, 데몬을 재시작하고, 다른 피어로 가는 경로를 망가뜨리지 않았는지 확인하는 것"으로 변했습니다.

NetBird는 WireGuard 위의 관리 평면입니다. 기술적으로 내부적으로는 동일한 wg를 사용하지만:

  • 노드는 신호 서버를 통해 서로를 찾고, NAT-traversal이 자동으로 이루어집니다.
  • SSO(저는 Authentik 사용)를 통해 인증하며, 키 공유가 없습니다.
  • 접근은 웹 인터페이스를 통해 정책으로 관리되며, 머신 그룹 및 그 사이의 규칙을 만들 수 있습니다.
  • 내장 SSH 프록시: ssh user@machine.netbird.cloud로 접속하여 SSO 인증을 받을 수 있습니다 (Authentik에서 승인). 노드에서 외부로 포트 22를 열 필요조차 없습니다.

NetBird 에이전트를 설치했습니다:

  • Proxmox 호스트에
  • 관리자 또는 다른 VM에 표시되어야 하는 모든 VM에
  • 출력 노드에
  • 제 노트북에

결과적으로 노트북에서 어떤 머신으로든 포트 전달, 방화벽 구멍, 추가 바스티온 호스트 없이 하나의 명령으로 접속할 수 있습니다. 그리고 사설 VM에는 외부로 열린 SSH가 하나도 없습니다 — 모두 vmbr1과 메쉬 wt0에만 연결됩니다.

가상 머신 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-FPM 및 Caddy를 VM에서 사용하는 오래된 PHP 포럼 (IPS). Docker로 패키징하고 싶지 않았습니다 — 너무 많은 레거시가 있고, 그대로 두는 것이 더 쉬웠습니다.
  • dokploy — Git을 통한 배포용 사용자 애플리케이션(Next.js, Postgres, Redis)용 Docker Swarm + Dokploy.

LXC:

  • pbs — Proxmox 백업 서버 (아래 참조).
  • pg16, pg17, pg18 — 서로 다른 메이저 버전의 세 개 Postgres 인스턴스. 이유: 여러 애플리케이션이 다른 메이저 버전을 요구하고, 각 애플리케이션마다 업그레이드/다운그레이드하는 것을 원치 않습니다.
  • dashboards — Homarr (시작 페이지), Uptime Kuma 및 Beszel 허브가 포함된 하나의 컨테이너. 이 세 가지는 논리적으로 동일한 것(관찰)에 관한 것이므로 하나의 compose로 묶었습니다.

각 VM은 10.10.10.x의 고정 IP(범위별로 구분, 찾기 쉬움), 2-8 vCPU 코어 및 부하에 따라 2-8GB RAM을 받습니다. 코어는 안전하게 과할당할 수 있습니다 — VM에 할당된 총 코어 수가 물리적인 코어 수보다 많지만, 동시에 CPU에 부하가 걸리지 않으면 완벽하게 작동합니다.

리버스 프록시: 2단계 Caddy

Proxmox 내부에는 VM이 아닌 호스트 자체에 Caddy를 설치했습니다. 왜냐하면 전용 서버의 공개 포트를 수신하고 Host 헤더에 따라 적절한 VM으로 라우팅해야 하는데, 이를 위해 포트 전달 기능이 있는 별도의 VM을 만드는 것은 불필요한 계층이기 때문입니다. Caddy는 가볍고 Go로 작성되었으며 systemd 유닛이 있고 구성 파일은 하나입니다.

이 호스트 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를 가진 제공업체에 위치합니다. 공개 도메인의 DNS는 이 노드를 가리킵니다.
  2. 단일 지점의 TLS 종료. 인증서는 이곳에서만 발급되며, Let's Encrypt는 이곳의 IP만 챌린지로 확인합니다. 이를 통해 모든 DNS-cleared check가 단순화되고 전용 서버가 80/443 포트로 인터넷에서 완전히 숨겨질 수 있습니다.
  3. 추가적인 격리 계층. DDoS 공격이 시작되면, 전용 서버가 아닌 출력 노드가 공격받습니다. 출력 노드에는 최소한의 데이터만 있으며, 재구축하는 데 10분이 걸립니다.
  4. 공개 프론트엔드와 백엔드 분리. 전용 서버에서 Caddy 업데이트/실험을 안전하게 수행할 수 있으며, 인터넷과의 실제 인터페이스는 별도로 안정적으로 유지됩니다.

단점: 추가 홉으로 인해 각 요청에 ~5-15ms의 지연이 발생합니다. 웹에서는 눈에 띄지 않습니다.

또한 일부 서비스의 경우 호스트의 :80/443에 직접 Caddy가 설치되어 있으며, 출력 노드를 통과할 필요가 없는 도메인(예: 상태 엔드포인트, 모니터링, DNS가 이미 전용 서버를 가리키는 서비스)에 사용됩니다. 이는 병렬로 작동합니다: 호스트의 Caddy는 :8080(출력 노드에서 내부로)과 :80/:443(인터넷에서 직접)을 모두 수신하며, 라우팅 규칙이 다릅니다.

마이그레이션: 눈물 없이 거의 다운타임 없이 이전하기

이것이 가장 어려운 단계였습니다. 다섯 대의 서버, 약 30개의 서비스, 실시간 사용자가 있는 프로덕션 포럼, 절대 잃어서는 안 되는 활성 구독이 있는 빌링, 데이터베이스.

전략은 서비스별로 하나씩 마이그레이션하는 것이었습니다. "가장 덜 중요한 것부터 가장 중요한 것까지" 순서로:

  1. 먼저 사소한 도구와 정적 웹사이트를 이전했습니다 (문제가 생겨도 아무도 눈치채지 못합니다).
  2. 그 다음 봇과 백그라운드 서비스를 이전했습니다 (이것도 몇 분간의 다운타임은 견딜 수 있습니다).
  3. 그 다음 데이터베이스를 이전했습니다 (사전에 복원 테스트 완료).
  4. 그 다음 해당 데이터베이스를 사용하는 애플리케이션을 이전했습니다.
  5. 마지막으로, 마지막 동기화 라운드와 DNS 전환을 포함한 주요 포럼을 이전했습니다.

마이그레이션 채널

먼저 NetBird를 구형 서버에 설치했습니다. 이로 인해 두 가지 문제가 즉시 해결되었습니다. 내부 네트워크에 안전하게 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은 전환 하루 전에 미리 60-300초로 낮췄습니다.

끊임없이 게시하는 활성 사용자가 있는 Discourse의 경우 다음과 같이 했습니다:

  1. 새 위치에서 모든 것을 설정했습니다.
  2. 유효성 검사(로그인, 게시, S3에 파일 업로드, 검색)를 실행했습니다.
  3. 구형 설치에서 read_only_mode를 활성화했습니다 (Discourse는 이 기능을 기본 제공합니다).
  4. 최종 rsync 업로드 + 최종 DB 덤프를 실행했습니다.
  5. 새 서버에서 시작하고 read-only 모드를 비활성화했습니다.
  6. DNS를 전환했습니다.
  7. 구형 서버는 일주일 동안 그대로 두었습니다 — "만약을 위해서".

사용자의 실제 다운타임 — 서비스당 약 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 덤프를 통해 이전했을 때, 데이터베이스에는 이전 로컬 레지스트리에 대한 링크가 이전 IP로 남아 있었습니다. 서비스가 시작될 때 100.76.117.115:5000으로 접속하여 No such image 오류와 함께 실패했습니다. 해결책 — Dokploy UI에서 각 애플리케이션을 다시 빌드하는 것입니다. 로컬 빌드는 이미지를 새 서버에 저장합니다.

백업: 동일 전용 서버 내 PBS + 오프사이트

이것은 제 자신과의 철학적 논쟁이었습니다: 같은 하드웨어에 백업할 것인가, 아니면 아닐 것인가. 답변: 예, 그리고 아니오.

레벨 1 — 풀 자체의 ZFS 스냅샷. 거의 무료(copy-on-write)이고, 즉시 생성되며, 즉시 복원됩니다. 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 백업 서버 설치. 이것은 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 복원 — 말 그대로 한 명령으로 가능:
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와 같은 콤바인을 모으는 것을 좋아하지 않습니다. 그래서 세 가지 가벼운 도구를 하나의 LXC에 집중했습니다:

  • Beszel — 각 VM의 에이전트(CPU, RAM, 디스크, 네트워크 인터페이스, Docker 컨테이너)로부터 메트릭을 수집합니다. 허브는 LXC에 있고, 에이전트는 각 VM/LXC의 systemd 유닛을 통해 실행됩니다. Beszel은 PocketBase를 통해 자체 인증을 사용하는데, 때로는 불편하지만 (아래의 함정 참조) 전반적으로 작동합니다.
  • Uptime Kuma — HTTP/HTTPS/Ping/TCP 프로브. 모든 공개 도메인(도메인), 모든 내부 서비스(NetBird 경유), 모든 메쉬 노드의 핑을 설정했습니다. 알림은 Telegram으로 전송됩니다.
  • Homarr — 모든 관리자 인터페이스에 대한 링크가 있는 시작 페이지입니다. PBS UI, Dokploy UI, Vaultwarden UI가 어떤 포트에 있는지 기억할 필요가 없습니다. 그냥 Homarr을 열고 클릭합니다.

이 세 가지 도구는 하나의 Docker compose, 하나의 LXC에 있으며 NetBird를 통해서만 접근 가능합니다.

Beszel 함정, 여러분이 겪지 않도록: Beszel은 PocketBase에 두 개의 사용자 테이블을 가지고 있습니다 — _superusers (CLI/API용) 및 users (웹 UI용). CLI superuser upsert를 통해 비밀번호를 재설정하면 _superusers만 재설정되며, UI에서는 users를 통해 로그인하므로 비밀번호가 맞지 않습니다. REST API를 통한 PATCH 요청 /api/collections/users/records/<id>로 해결할 수 있습니다.

최종 결과

약 3주간의 마이그레이션 후, 마지막 서비스들이 안정화되었을 때, 다음과 같은 결과를 얻었습니다:

매개변수이전현재
서버 / 청구서3개 호스팅 업체의 5개1 전용 서버 + 1 마이크로 VPS
월별 지불~$X~$X/2
사용 가능한 리소스"충분한 것 같다"8 vCPU 및 30 GB RAM 여유
백업각 VPS의 S3 rsync 크론PBS + 오프사이트 + ZFS 스냅샷
복원"글쎄, 하루 정도 걸린다"PBS에서 VM당 2-5분
SSH 접속5개의 서로 다른 포트 및 키SSO가 있는 단일 NetBird 메쉬
서비스 격리공유 쓰레기통각 VM/LXC별 격리
업그레이드 전 스냅샷"기도해라"qm snapshot 201 pre-update
루틴 작업을 위한 웹 인터페이스어떤 웹 인터페이스?Proxmox UI

이것이 제게 준 가장 큰 감정적 이득은 무언가를 건드리는 것을 더 이상 두려워하지 않게 되었다는 것입니다. 모든 위험한 작업(업그레이드, 마이그레이션, 실험)은 이제 스냅샷으로 시작하여 30초 만에 커밋하거나 롤백하는 것으로 끝납니다. 실수 비용이 크게 줄었기 때문에 새로운 것을 시도하는 빈도가 늘었습니다.

반복할 경우 체크리스트

만약 당신도 저와 같은 증상을 겪고 있다면 — 따라야 할 순서에 대한 간단한 체크리스트입니다:

  1. 모든 VPS의 총 지불액을 계산하고 OVH/Hetzner/LeaseWeb의 전용 서버 가격과 비교하십시오. 놀라게 될 것입니다.
  2. ZFS를 사용할 계획이라면 ECC RAM을 선택하십시오. 아끼지 마십시오.
  3. 최소 두 개의 디스크를 미러링하여 사용하십시오. 단일 디스크는 프로덕션 환경에 적합하지 않습니다.
  4. rescue + debootstrap이 아닌 KVM/IPMI를 통해 Proxmox를 설치하십시오. 고통이 줄어듭니다.
  5. 즉시 SSH 키, 비표준 포트, 비밀번호 비활성화, fail2ban 설치를 구성하십시오.
  6. 즉시 ZFS ARC를 제한하십시오. 기본적으로 RAM의 절반을 사용합니다.
  7. 비공개 네트워크와 NAT를 위한 두 번째 브리지 vmbr1 만드십시오. 모든 VM/LXC는 여기에 연결합니다. 가상 머신에는 공개 IP를 할당하지 마십시오.
  8. 마이그레이션을 시작하기 전에 NetBird(또는 Tailscale, Headscale)를 설정하십시오. 이것이 마이그레이션 채널이자 관리자 액세스입니다.
  9. Proxmox UI를 외부에 열지 마십시오. VPN 메쉬를 통해서만 접근하십시오.
  10. TLS 종료 및 차단 우회를 위한 다른 제공업체의 작은 출력 VPS 하나 — 필수 사항은 아니지만 매우 편리합니다. 비용은 적고 이점은 많습니다.
  11. 별도 LXC의 PBS + S3로의 오프사이트 동기화. 백업은 자동화되어야 합니다.
  12. 최소 한 달에 한 번 테스트 복원을 수행하십시오. 그렇지 않으면 백업이 아니라 자기 위안용 파일입니다.
  13. 서비스별로 하나씩 중요도 순으로 마이그레이션하십시오. "주말 동안 모든 것을 옮기려고" 하지 마십시오.
  14. 전환 하루 전에 DNS TTL을 낮추십시오. 한 시간 전이 아닙니다.
  15. 각 서비스에 대한 문서를 작성하십시오: 구성 파일 위치, 시작 방법, 백업 방법, 복원 방법. 6개월 후에 스스로에게 감사하게 될 것입니다.

아직 완료되지 않은 작업

완전성을 위해: 제 마이그레이션은 아직 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