EST. 2026 — Notes from real server work SYSADMIN · SECURITY · AI · DIRECTADMIN Issue 01
THE SYSADMIN ALMANAC
Home / DirectAdmin / DA CentOS 7 → Debian 12 on Proxmox Part 1 — NAT pre-stage
DirectAdmin — ซีรีส์ ย้าย DA CentOS7 -> Debian12 [1]

DA CentOS 7 → Debian 12 on Proxmox Part 1 — NAT pre-stage

by Ton-Or ·20 May 2026·11 min read
DA CentOS 7 → Debian 12 on Proxmox Part 1 — NAT pre-stage

My customer's main server had been running CentOS 7 for almost ten years. It reached EOL in mid-2024, and I put off the migration for quite a while before getting to it. The migration is now done, and the server has been stable in production for almost a month, so I'm writing up the approach I used.

My customer's main server had been running CentOS 7 for almost ten years. It reached EOL back in mid-2024, and I had put this off for quite a while. There are over 80 users on this machine, all waiting to move to a new OS. I finished the work late last month. The server is stable now, with almost a month in production and no problems so far.

In this series, I'll walk through the entire migration from start to finish. For this first part, I'll start with NAT pre-stage, which is central to reducing downtime.

The idea: prepare the new VM before shutting down the old one

The principle is simple. Instead of shutting down the old VM and then building a new one (which would leave customers waiting for hours), I chose to build the new VM on a separate internal bridge, in a private subnet that does not conflict with the old machine's public IP.

While I prepare the new VM, the old VM stays online and serves customers as usual. It has no idea another machine is being prepared alongside it. Once the system is ready and the process is nearly complete, I can swap the NICs in a short window.

I'll be upfront here — I initially expected this technique to bring customer downtime close to zero. But in practice, I hit two obstacles: the DA Legacy license and slow qcow2 reads. Actual downtime ended up being about 1 hour. I'll cover the details in full at the end; for now, I'll focus on the preparation.

The 2-bridge layout

My network layout uses two bridges on the Proxmox host: one public bridge and one internal bridge with outbound NAT.

Network layoutInternet->PVE hostvmbr0 / public IP->NAT MASQvmbr1->New VM192.168.10.20
vmbr0

public bridge

  • PVE host + old VM connect here
  • Uses public IPs 203.0.113.x
  • Customers connect through here as usual
vmbr1

internal bridge

  • private 192.168.10.0/24
  • NAT egress through vmbr0
  • The new VM stays here during preparation

The key is that the new VM connects to vmbr1, so it never conflicts with the old VM's public IP. This lets both machines run in parallel on the same host.

InternetPVE hostvmbr0 public bridge (bridge-ports eno1)Old VMServing customers as usualNAT MASQUERADEDNAT ssh port to new VMvmbr1 internal bridge 192.168.10.0/24New VM 192.168.10.20bridge-ports none: no physical NIC
The old VM is on vmbr0 with a public IP, while the new VM is on vmbr1 without conflicting with the existing IP. It reaches the internet through MASQUERADE and accepts outside connections through DNAT on the host, allowing both machines to run in parallel.

Configure networking on PVE — /etc/network/interfaces

I configure vmbr0 as the public bridge connected to the host's physical NIC. vmbr1 is a standalone bridge with no physical port, but it serves as the private subnet's gateway, with MASQUERADE through vmbr0.

auto vmbr0
iface vmbr0 inet static
    address 203.0.113.41/29
    gateway 203.0.113.1
    bridge-ports eno1
    bridge-stp off
    bridge-fd 0

auto vmbr1
iface vmbr1 inet static
    address 192.168.10.254/24
    bridge-ports none
    bridge-stp off
    bridge-fd 0
    post-up   iptables -t nat -A POSTROUTING -s 192.168.10.0/24 -o vmbr0 -j MASQUERADE
    post-up   iptables -A FORWARD -s 192.168.10.0/24 -j ACCEPT
    post-up   iptables -A FORWARD -d 192.168.10.0/24 -j ACCEPT
    post-down iptables -t nat -D POSTROUTING -s 192.168.10.0/24 -o vmbr0 -j MASQUERADE
    post-down iptables -D FORWARD -s 192.168.10.0/24 -j ACCEPT
    post-down iptables -D FORWARD -d 192.168.10.0/24 -j ACCEPT

bridge-ports none is the key here. It tells vmbr1 not to connect to any physical NIC; it is entirely internal to the host. The MASQUERADE rule rewrites the source address of every packet leaving 192.168.10.0/24 to the host's IP so it can go out through vmbr0 — the new VM can therefore access the internet normally and download packages to install, even without a public IP of its own.

Then I bring it up with ifup vmbr1 and check that the bridge is up and the NAT rule is in place.

ifup vmbr1
ip a show vmbr1
iptables -t nat -L POSTROUTING -n | grep MASQ

If address 192.168.10.254 appears on vmbr1 and there is a MASQUERADE line in the POSTROUTING chain, the network side is ready.

SSH access to the new VM through PVE

Now that the new VM is in a private subnet, I cannot ssh directly to it from outside. I need DNAT forwarding through the host. I choose a port on the host (let's call it <fwd-port>) and point it to port 22 on the new VM.

iptables -t nat -A PREROUTING -p tcp --dport <fwd-port> -j DNAT --to 192.168.10.20:22
iptables -A FORWARD -p tcp -d 192.168.10.20 --dport 22 -j ACCEPT
netfilter-persistent save

netfilter-persistent save matters here; otherwise, the rules disappear on reboot. Then I test from my workstation.

ssh -p <fwd-port> [email protected]

If everything is correct, I can connect through NAT forwarding straight to the new VM's shell, even though it still has no public IP of its own.

Create the new VM on vmbr1

The next step is to create the VM and connect it directly to vmbr1. I use qm create with the specifications the customer needs.

qm create 101 \
  --name newserver \
  --memory 60000 \
  --cores 16 \
  --machine q35 \
  --bios seabios \
  --scsihw virtio-scsi-single \
  --net0 virtio,bridge=vmbr1 \
  --scsi0 VMDATA:900,discard=on,ssd=1,iothread=1

The --net0 ...,bridge=vmbr1 part connects the VM to the internal bridge, placing it in the private subnet from the start.

There are 4 gotchas at this step that I've run into before, so here is what to watch for.

4 things that cost me time

1) Do not add viommu=on — the installer will hit a kernel panic on Debian 12 + q35. It took me 2 hours to identify this as the cause. 2) Use q35, not i440fx — it provides modern PCIe and better ZFS performance. 3) The NIC name on Debian 12 + q35 + virtio is enp6s18, not ens18 as on Debian 13. Anyone used to ens18 will get confused. 4) Set virtio queues=8 later to use all vCPUs concurrently.

I configure virtio multiqueue later with this command.

qm set 101 --net0 virtio,bridge=vmbr1,queues=8

Setting queues equal to the vCPU count helps spread traffic processing across cores instead of piling it onto a single core at high throughput.

Install Debian 12 minimal

I boot from the ISO and choose a minimal installation. I select only SSH server and standard system utilities. There is no need to install a desktop or any unnecessary services; CustomBuild will install what is needed later.

For partitions, I use roughly this layout.

500M/boot
200G/
Largest/home
8Gswap
8G/tmp

I make /home the largest because the data for all 80-plus users will live there.

Quota mount option — easy to miss

This is critical for DirectAdmin: the / and /home partitions need quota mount options.

usrjquota=aquota.user,grpjquota=aquota.group,jqfmt=vfsv0
Use vfsv0, not vfsv1

I chose vfsv0 even though newer DA docs recommend vfsv1, because vfsv0 is compatible with the existing CentOS 7 quota file format. When restoring customer quotas, the restore fails with vfsv1. I spent time trying it once before switching back to vfsv0.

Install base packages

After installing the OS, I first install the base packages that DA and CustomBuild need.

apt update && apt install -y \
    openssh-server curl wget quota iptables chrony \
    perl python3 sudo rsync bzip2 \
    netfilter-persistent

systemctl enable --now chrony
chronyc sources -v
chrony matters for the DA license

The system clock must be accurate because DA license validation also checks the time. I once forgot to configure chrony, and license activation failed because the time skew was too large. I spent a long time troubleshooting before realizing it was the clock.

The benefit: all preparation happens without affecting customers

This is the real value of NAT pre-stage — I can perform every preparation step below on the new VM while customers continue using the old machine.

  • Debian 12 install + base packages — around 30 minutes
  • DA install + license activate — around 10 minutes (the old DA panel goes offline for just 1 minute, but customer services keep running normally)
  • CustomBuild stack — around 2 to 2.5 hours
  • custom data prep (DNSSEC keys, hooks, templates) — around 30 minutes
  • firewall config staging — around 10 minutes
  • pre-cutover smoke test — around 30 minutes

That adds up to around 3 to 4 hours of preparation — customers are completely unaffected during this time because everything happens on a parallel machine tucked away in a private subnet.

But the cutover still takes time, not zero

I initially hoped downtime would be close to zero, but that did not happen in practice because of two obstacles.

Obstacle 1

DA Legacy license

  • Uses last DA wins behavior
  • Cannot run two DA instances at once
  • Must stop the old DA before activating the new one
Obstacle 2

Slow qcow2 reads

  • DA backup on the old machine takes hours
  • The main bottleneck during cutover
  • This is why ZFS comes into play in the next part

Together, these two issues brought actual customer downtime to about 1 hour, instead of the zero I had initially aimed for. I'll cover the full cutover details at the end of the series.

Next part

In the next part, I'll cover storage — I'll remove the old mdadm setup and replace it with a ZFS mirror, all while the old VM remains online and serves customers.

Feel free to adjust the subnet, port, or specifications to suit your own machine. The principle is the same — get the new machine ready first, then swap over later to minimize the impact on customers. If I think of anything else or find something that needs correcting, I'll update this article.

Want us to set this up?

Get help with server setup, configuration, migration, or troubleshooting. Save time for the work that matters.

Get a quote

Hit something like this? Stuck somewhere?

Come trade notes on the LinuxThai board

→ Continue on the board

Comments 0

No comments yet — be the first