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

How I Moved from Five VPSs to One Dedicated Server with Proxmox

@dignezzz · author17 min read2026-05-05free

TL;DR: TL;DR: Fragmented infrastructure from five VPSs across three providers → one dedicated server Ryzen 7 9700X / 64GB ECC / 2x NVMe ZFS mirror, running Proxmox VE. Inside: VMs/LXCs by role, NetBird mesh instead of port forwarding, an exit node on a separate VPS for bypassing blocks and TLS termination, PBS for backups. Profit in money, control, and peace of mind. This article details the why, how, and the pitfalls I encountered along the way, so you don't have to.

How I Moved from Five VPSs to One Dedicated Server with Proxmox and Stopped Feeding Hosting Providers

TL;DR: Fragmented infrastructure from five VPSs across three providers → one dedicated server Ryzen 7 9700X / 64GB ECC / 2x NVMe ZFS mirror, running Proxmox VE. Inside: VMs/LXCs by role, NetBird mesh instead of port forwarding, an exit node on a separate VPS for bypassing blocks and TLS termination, PBS for backups. Profit in money, control, and peace of mind. This article details the why, how, and the pitfalls I encountered along the way, so you don't have to.

Why Did I Even Start This?

By the end of last year, I realized I was paying for five different VPSs from three hosts. One for a Discourse forum, the second for a legacy PHP forum with MySQL, the third for billing and Telegram bots, the fourth for VPN/proxy, and the fifth for static files and various small tools. Also, n8n was running somewhere there, along with Vaultwarden and a couple of Uptime Kuma instances that were monitoring each other circularly (how else would it be done?).

What was wrong:

  • Money. Cumulatively, it was about the same cost as one decent dedicated server with similar specs combined (but those were shared, these are dedicated).
  • No isolation. If one client's bot starts leaking memory, my forum starts lagging. On the same server. Lovely.
  • Backups - a mess. Each VPS had its own rsync cron job to S3, different retention policies everywhere, no incrementality, restoring was a whole adventure.
  • Network salad. SSH through different ports, forwarded via Cloudflare Tunnels, through manually set up WireGuard between two VPSs, with configs in three places. Every time I added a new machine, it took a day to integrate.
  • Update apocalypse. With five different Ubuntu/Debian systems with various versions and packages, upgrades had to be done manually and never simultaneously. Every six months, I heroically completed the quest of "update everything, break nothing."
  • CPU and RAM idle. Each VPS was booked "for peak load," while on average, it was used at 15-20%. Money was flying into the void.

In short — a classic case where infrastructure grew based on the principle of "Oh, I need another service — I'll buy a VPS." It was time to consolidate all of this.

What I Chose and Why

Hardware

I opted for a dedicated server:

  • CPU: AMD Ryzen 7 9700X (8 cores / 16 threads, Zen 5)
  • RAM: 64 GB DDR5 ECC 5200 MHz
  • Disks: 2x 512 GB enterprise NVMe → ZFS mirror
  • Network: 1 Gbps unmetered
  • Price: approximately the cost of two mid-range VPSs from the same provider (OVH)

Hypervisor

Proxmox VE 9.x. I considered alternatives, but briefly:

  • Bare Docker — no isolation, no proper snapshots, backup only via compose means, disk is a single mess.
  • VMware ESXi — no longer free, Broadcom's licensing is delightful, thank you.
  • XCP-ng — decent, but personally, I know Proxmox inside out, and its community is more active.
  • Kubernetes — nope, I'm one person, I don't need to build a rocket to go to the store.

Proxmox gives me everything I need: KVM virtual machines for everything that requires isolation (Docker, specific kernels, different distributions); LXC containers where resources can be saved (databases, monitoring, small services); built-in PBS for backups; ZFS out of the box; snapshots; live migration (if I ever get a second dedicated server); a web interface where I can click with a mouse when I don't want to type in the terminal.

Architecture: General Scheme

First, I'll show what I ended up with, then I'll explain how I got there.

The logic is as follows:

  1. On the dedicated server runs Proxmox. It holds the public IP Y.Y.Y.Y on vmbr0.
  2. A second bridge, vmbr1, is created inside with a private network 10.10.10.0/24. This is an analog of an "internal LAN" — all VMs and LXCs are connected there, accessing the outside world via NAT from the host; they have no incoming public ports.
  3. Only three things run on the host itself: Caddy (internal reverse proxy), nftables (firewall), and NetBird (VPN agent). No Docker on the host, no application logic on the host. The host is a sacred cow; its job is to be a hypervisor and not crash.
  4. Virtual machines are separated by roles: one for the forum, one for background services and bots, one for Dokploy with user applications, etc. They have direct visibility to each other via 10.10.10.x, and access to the outside world is only through the host's Caddy.
  5. LXC containers are for everything "lightweight": multiple Postgres instances, Proxmox Backup Server, monitoring dashboards. They are also on vmbr1.
  6. A separate exit node ("Peer Caddy") is a small VPS from another provider, in a network suitable for my audience. It runs Caddy, which terminates TLS (Let's Encrypt) and proxies HTTP to the dedicated server via the NetBird mesh. DNS for all public domains points to this VPS, not the dedicated server.
  7. NetBird mesh connects the dedicated server, the exit node, and my laptop (plus a couple of old servers that haven't been migrated yet). It's WireGuard over SSO authentication, without externally open ports.

Why such isolation through an exit node is a separate discussion, which I will return to.

Proxmox Installation: How I Set It Up

I ordered the dedicated server and on the first boot, installed Proxmox using rescue mode/KVM (OVH offers both; KVM is more convenient for me as I can see the entire process). During installation:

  • ZFS RAID1 on both NVMe drives (ashift=12, compression=lz4 enabled immediately).
  • Default partitioning; Proxmox automatically creates rpool/ROOT, rpool/data, and swap.

Next – basic cleanup after installation. This is a must-have; without it, Proxmox throws subscription errors and doesn't perform optimally.

bash
1# 1. Remove enterprise repo (no subscription)
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. Remove the 'No valid subscription' nag window
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. Hostname / timezone
13hostnamectl set-hostname dedik-waw
14timedatectl set-timezone Europe/Warsaw

SSH Hardening

Immediately. Not "later." Later never comes.

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

I closed port 22 in the firewall, leaving only the non-standard one. This isn't security through obscurity; it's reducing background noise in logs — port 22 receives 50,000 brute-force attempts within the first hour, making the logs unreadable. Port 22222 is quiet, allowing for real anomaly monitoring.

ZFS - Tuning for NVMe and 64GB RAM

ZFS by default wants to consume half of the RAM for ARC. This is fine for a file server but absolutely unacceptable for a hypervisor where VMs need memory. We'll cap ARC to a reasonable 16 GB:

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

My memory distribution:

  • ~16 GB — ZFS ARC
  • ~44 GB — VM/LXC
  • ~4 GB — system, buffers, overhead

Useful pool settings:

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    # for VM disks
8zfs set sync=standard rpool/data
9
10zpool set autotrim=on rpool          # critical for NVMe

compression=lz4 saves 20-30% space with almost no CPU overhead. recordsize=64K for VM block devices is the best compromise between performance and write amplification.

Internal Network and Firewall

Created a second bridge for private networking with NAT to the outside:

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

Now any VM/LXC on bridge vmbr1 gets a local address 10.10.10.x and accesses the internet through the host, but is invisible from the outside.

nftables with policy drop. Only what should be open is open; the rest is silently dropped. Minimal set:

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 (only from NetBird, see below)
12        tcp dport { 80, 443, 8080 } accept   # Caddy
13        udp dport 29899 accept           # NetBird
14
15        iif vmbr1 accept                 # internal network - all allowed
16        iif wt0 accept                   # NetBird interface - also trusted
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}

I physically blocked access to the Proxmox UI (port 8006) from the internet via CF/firewall and access it only through NetBird. The public IP doesn't respond to the web interface at all. The same applies to the PBS UI, dashboards, and all admin interfaces — only via the mesh. Only truly public service ports are open from the outside.

NetBird Mesh: Why Not Classic WireGuard?

I used regular WireGuard for a long time, and it was fine until I had more than three servers. After that, managing configurations became painful: every peer addition turned into "update config on all nodes, don't forget allowed_ips, restart the daemon, check that you haven't broken the route to another peer."

NetBird is a management layer on top of WireGuard. Technically, under the hood, it's still wg, but:

  • nodes find each other via a signaling server, NAT traversal is automatic.
  • authentication via SSO (mine is through Authentik), without sharing keys.
  • access is managed in the web UI via policies; you can create groups of machines and rules between them.
  • built-in SSH proxy: you can connect via ssh user@machine.netbird.cloud and get SSO authentication (with approval in Authentik). You don't even need to open port 22 on the node externally.

I installed the NetBird agent:

  • on the Proxmox host.
  • on each VM that needs to be visible to the administrator or other VMs.
  • on the exit node.
  • on my laptop.

As a result, I can connect to any machine from my laptop with a single command, without port forwarding, without firewall holes, without additional bastion hosts. And there's no open SSH on any of the private VMs — they are all only on vmbr1 plus the mesh wt0.

VMs vs. Containers: How I Decided for Myself

A simple rule that works almost always:

WhenWhat to Use
Docker workloadsVM (Docker in LXC can be capricious, especially with overlay2)
Databases without DockerLXC (lighter, faster, direct disk access via ZFS)
System services (PBS, monitoring, small tools)LXC
Applications requiring their own kernel / network tricksVM
VPN nodes (WG, AmneziaWG)LXC (minimal overhead)

In the end, I ended up with something like this:

Virtual Machines (KVM):

  • forum — Discourse in a standard launcher container. Docker inside.
  • services — lots of small things: webhook bots, AI bot, billing panel, Vaultwarden, Uptime Kuma. All in Docker Compose, one folder per service.
  • legacy — old PHP forum (IPS) with native PHP-FPM and Caddy on the VM. Didn't want to package it in Docker — too much legacy, easier to leave it as is.
  • dokploy — Docker Swarm + Dokploy for user applications (Next.js, Postgres, Redis), deployed via Git.

LXC:

  • pbs — Proxmox Backup Server (see below).
  • pg16, pg17, pg18 — three separate Postgres instances of different major versions. Reason: different applications require different major versions, and I don't want to manage them all together.
  • dashboards — one container with Homarr (start page), Uptime Kuma, and Beszel hub. All three in one compose file because they logically belong together — monitoring.

Each VM gets a static IP in 10.10.10.x (categorized by ranges for easier searching), 2-8 vCPUs, and 2-8 GB RAM depending on the load. Cores can be safely overprovisioned — I've allocated more cores to VMs than physically available, and it works perfectly as long as no one hits the CPU limit simultaneously.

Reverse Proxy: Two-Tier Caddy

Inside Proxmox, I installed Caddy on the host, not in a VM. Why? Because it needs to listen on the dedicated server's public ports and route by Host header to the correct VM. Setting up a separate VM with port forwarding for this would be an unnecessary layer without benefit. Caddy is lightweight, written in Go, has a systemd unit, and a single config file.

This host Caddy listens only on port 8080 HTTP and routes to backends via 10.10.10.x. It has no TLS on it.

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}

And TLS termination and certificates are on a separate exit node, in Docker, on a similar principle:

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 is the NetBird address of the dedicated server. The request arrives at the exit node via HTTPS, is decrypted, goes into the NetBird mesh via HTTP to the dedicated server, where Proxmox Caddy matches by Host and routes it to the internal VM. Between the exit node and the dedicated server, it's HTTP by design — we trust the mesh, there's no external traffic here.

Why an Exit Node at All?

Several reasons:

  1. Bypassing blocks. Some OVH subnets are blocked in Russia. The exit node is with a provider whose IP is not blocked. DNS for public domains points there.
  2. TLS termination in one place. Only it issues certificates; only its IP is used for Let's Encrypt challenges. This simplifies all DNS-cleared checks and allows the dedicated server to be completely invisible from the internet on ports 80/443.
  3. Additional isolation layer. If someone starts a DDoS attack, they attack the exit node, not the dedicated server. I have minimal data on the exit node; recreating it takes 10 minutes.
  4. Separating public front-end from the back-end. I can safely perform Caddy updates or experiments on the dedicated server while the actual internet interface is separate and stable.

The only downside is an increase of ~5-15 ms to each request due to the extra hop. This is unnoticeable for web traffic.

Also, for some services, there's a direct Caddy on the host on ports 80/443 without the exit node — for domains that I don't need to route through the exit point (e.g., status endpoints, monitoring, services where DNS already points to the dedicated server). This works in parallel: Caddy on the host listens on both :8080 (internal from the exit node) and :80/:443 (direct from the internet), with different routes.

Migration: How I Moved Without Tears and Almost No Downtime

This was the most nerve-wracking stage. Five servers, about thirty services, a production forum with live users, billing with active subscriptions, databases that absolutely cannot be lost.

Strategy — migrate one service at a time, in order of "from least critical to most critical":

  1. First, small tools and static sites were moved (if something broke, nobody would notice).
  2. Then, bots and background services (they can also tolerate a minute of downtime).
  3. Then, databases (with pre-tested recovery).
  4. Then, applications that depend on these databases.
  5. Finally, the main forum with a long final sync round and DNS switchover.

Migration Channel

First, I simply set up NetBird on the old servers. This solved two problems at once: I could safely SSH into internal networks, and rsync went over WireGuard via NetBird, without exposing data to the open internet.

The command used for each service was roughly:

bash
1# From the new dedicated server, via NetBird IP of the old server:
2rsync -avzP --delete \
3  -e "ssh -p 5322" \
4  root@100.76.108.210:/var/discourse/shared/standalone/ \
5  /target/discourse/shared/standalone/

For databases — dump + restore, not file copy. Never copy Postgres/MySQL files "hot"; it's a one-way ticket.

bash
1# On the old server
2docker exec -it shm-vsem-mysql mysqldump -u root -p shm-vsem | \
3  gzip > /tmp/shm-vsem.sql.gz
4
5# On the new server, via 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

For Discourse — native discourse backup / discourse restore via ./launcher enter app. Internally, it correctly collects the Postgres dump + uploads + configs, and restores them in the same way.

DNS Switchover

After setting up the service in the new location and verifying it worked via the internal address, I ran both installations in parallel for 5-15 minutes to check data synchronization, and then switched the DNS to the new server. I had previously lowered the TTL for records to 60-300 seconds a day before the switch.

For Discourse, where users are constantly posting, I did the following:

  1. Set everything up in the new location.
  2. Ran validation (login, posting, uploading files to S3, search).
  3. Enabled read_only_mode on the old installation (Discourse supports this out of the box).
  4. Performed a final rsync of uploads + a final database dump.
  5. Started the new installation, disabled read-only mode.
  6. Switched DNS.
  7. Left the old installation untouched for another week — just in case.

The actual downtime for users was about two minutes per service.

Surprises Along the Way

  • Permissions during pct restore. LXC under unprivileged 1 maps UIDs with an offset of 100000. If your files on the old server were under UID 1000, on the new server they will be under UID 101000, and the application won't see them. This is resolved either by chown after restore or by using --unprivileged 0 if you understand the risks.
  • Docker IPv6 in OVH VMs sometimes worked incorrectly — fixed by disabling IPv6 in the Docker daemon ("ipv6": false).
  • Time on VMs. Several times, I noticed discrepancies in system time between the host and VMs, which broke TLS and signatures. The fix: systemd-timesyncd or chrony on each VM, and host.use-time in the Proxmox agent.
  • Discourse migration between two versions. If the Discourse version on the old server is newer than the one you installed on the new server, restore will fail. The version must be the same or newer on the new installation.
  • Image registry for Dokploy. When I migrated Dokploy metadata via dokploy-postgres dump, the database still contained links to the old local registry on the old IP. Services attempting to start would connect to 100.76.117.115:5000 and fail with No such image. Solution: rebuild each application in the Dokploy UI; local build then pushes the image to the new server.

Backups: PBS on the Same Dedicated Server + Offsite

This was a separate philosophical debate with myself: back up on the same hardware or not. The answer: both yes and no.

Level 1 — ZFS snapshots on the pool itself. They are almost free (copy-on-write), instant to create, and instant to restore. I keep automatic snapshots of rpool/data every 15 minutes with a 24-hour retention, plus daily snapshots with a week's retention. Uses zfs-auto-snapshot. Protection against "Oops, I just deleted the production DB":

bash
1apt install zfs-auto-snapshot
2# After this, it automatically creates frequent/hourly/daily/weekly/monthly snapshots via cron.

Level 2 — Proxmox Backup Server in a separate LXC on the same host. These are full incremental backups of VMs/LXCs via vzdump, deduplicated at the chunk level. PBS stores snapshots in a separate ZFS dataset, sees them as a repository, and each VM has its own increment chain.

Configuration in Proxmox: Datacenter → Backup → Add. Schedule: every day at 4 AM, retention keep-daily=7 keep-weekly=4 keep-monthly=3. All done via mouse.

Placing PBS in an LXC on the same host is a compromise. Pro: no need for separate hardware, backup goes via localhost, speed is like local SSD, retention/dedup works perfectly. Con: if the entire dedicated server fails, the backups fail too. That's why there's...

Level 3 — synchronization of the PBS repository to offsite S3. PBS supports a sync job to S3-compatible storage. Every night, I upload snapshots to a separate bucket in Selectel's S3 (any provider works — Backblaze, Wasabi, Cloudflare R2). Retention there is two weeks, as more isn't needed: for long-term storage, there's the local PBS, and offsite is insurance against "the entire datacenter burned down."

Level 4 — application data separately. Discourse itself makes its own backups and stores them in S3 (this is its native feature). So, even if PBS and the entire dedicated server disappear, I still have consistent Discourse dumps in their cloud.

bash
1# Restoring an entire VM from PBS is literally one command:
2pvesm list backup-pbs    # check available backups
3qmrestore backup-pbs:backup/vzdump-qemu-201-2026_04_06-04_00_03.vma.zst 999 \
4  --storage local-zfs
5# And in a minute, you have a copy of the forum VM from 4 AM running on VMID 999.

This is critical: backups you haven't tested restoring are not backups. Once a month, I perform a test restore of a random VM to an empty VMID, check that it boots up, that the application within it works, and then delete it. Boring, but once it helped me discover that one of the cron jobs was writing to /tmp/..., which was skipped during backup, and the application required a manual step after restore.

Monitoring

I dislike assembling behemoths like Prometheus + Grafana + Alertmanager + Loki when I have only 15 machines. So, I settled on three lightweight tools in one LXC:

  • Beszel — collects metrics from agents on each VM (CPU, RAM, disks, network interfaces, Docker containers). The hub runs in LXC, agents on each VM/LXC via systemd unit. Beszel's authentication is its own, via PocketBase, which is sometimes inconvenient (see below about pitfalls), but it generally works.
  • Uptime Kuma — probes via HTTP/HTTPS/Ping/TCP. I've configured all public domains, all internal services (via NetBird), and pings to all mesh nodes. Alerts go to Telegram.
  • Homarr — a start page with links to all admin interfaces. So I don't have to remember which port PBS UI is on, or Dokploy, or Vaultwarden. I just open Homarr and click.

All three are in one Docker Compose file, in one LXC, accessible only via NetBird.

Beszel Pitfall for you not to step on: Beszel has two user tables in PocketBase — _superusers (for CLI/API) and users (for the web UI). If you reset the password via CLI superuser upsert — you only reset _superusers, but you log into the UI using users, and the password won't work. Fix this with a PATCH request to /api/collections/users/records/<id> via REST API.

What I Got in the End

After about three weeks of migration, when the last services settled down, I tallied the results:

ParameterBeforeAfter
Servers / Bills5 from 3 hosts1 dedicated server + 1 micro-VPS
Monthly Payments~$X~$X/2
Free Resources"seems enough"8 vCPUs and 30 GB RAM in reserve
Backupsrsync cron to S3 on each VPSPBS + offsite + ZFS snapshots
Restoration"well, about a day"2-5 minutes per VM from PBS
SSH Access5 different ports and keysOne NetBird mesh with SSO
Service IsolationShared messEach in its own VM/LXC
Snapshot before upgrade"pray"qm snapshot 201 pre-update
Web UI for routine tasksWhat web UI?Proxmox UI

The main emotional benefit I gained — I stopped being afraid to touch anything. Any risky action (upgrade, migration, experiment) now starts with a snapshot and ends either with a commit or a rollback in 30 seconds. I started trying new things more often because the cost of failure dropped significantly.

Checklist If You Plan to Repeat This

If you're experiencing the same symptoms as I was — here's a short checklist in order of what makes sense:

  1. Calculate your total VPS costs and compare them with dedicated server prices from OVH/Hetzner/LeaseWeb. You'll be surprised.
  2. Get ECC RAM if you plan to use ZFS. Don't skimp.
  3. Get at least two disks in a mirror. A single disk is not an option for production.
  4. Install Proxmox via KVM/IPMI, not through rescue + debootstrap. Less pain.
  5. Immediately configure SSH keys, a non-standard port, disable passwords, and install fail2ban.
  6. Immediately limit ZFS ARC. By default, it will consume half your RAM.
  7. Create a second bridge vmbr1 for a private network with NAT. Put all VMs/LXCs there. No public IPs on virtual machines.
  8. Set up NetBird (or Tailscale, or Headscale) before you start migrating. This will be your migration channel and admin access.
  9. Do not expose the Proxmox UI to the internet. Access it only through the VPN mesh.
  10. A small exit VPS from another provider for TLS termination and bypassing blocks is not mandatory but very convenient. The cost is negligible, the benefits are many.
  11. PBS in a separate LXC + offsite sync to S3. Backups must be automatic.
  12. At least once a month, perform a test restore. Otherwise, you don't have backups, just files for self-assurance.
  13. Migrate one service at a time, in order of increasing criticality. Don't try to "move everything over the weekend."
  14. Lower DNS TTL a day before switching, not an hour.
  15. Keep documentation for each service: where configs are, how to start, how to back up, how to restore. You'll thank yourself in six months.

What's Left to Do

For completeness: my migration is not 100% finished yet. Remaining tasks:

  • A couple of Remnawave panels from the old server (planned for next weekend).
  • Migrating Caddy with plugins (it's a custom build), it's temporarily working.
  • Old CrowdSec — setting it up from scratch, not migrating.

But all critical components (forums, billing, bots, databases) are already on the dedicated server, working stably, being backed up, and monitored.

If you have questions about specific steps, configurations, or pitfalls I encountered in more detail, please write in the comments, and I'll try to elaborate. I'm especially interested to hear how you solve similar tasks on other hypervisors (XCP-ng, ESXi, or bare Docker Swarm) — perhaps I'm missing something.

Good luck with consolidating your infrastructure. Fewer bills, more control — it's worth it.

~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