Ubuntu-24.04 Minimal Server on KVM/Libvirt Default NAT: Network Troubleshooting and Docker Deployment Full Record

This post is the complete record of placing Ubuntu 24.04 Minimal Server into a KVM/libvirt default NAT network — going from “apt update completely fails” to “Docker Engine running normally” — a deployment walkthrough you can follow step by step.

The focus is not just providing a set of commands, but documenting every checkpoint clearly, to avoid getting stuck at the same place when rebuilding the environment later.

Setting Up the Virtualization Server (Arch-Derived)

First, install virtualization and networking packages on the Host.

Host:

1
paru -S virt-manager qemu-desktop libvirt edk2-ovmf dnsmasq

Purpose:

  1. virt-manager: Graphical VM management.
  2. qemu-desktop: KVM/QEMU hypervisor.
  3. libvirt: VM and virtual network management core.
  4. edk2-ovmf: UEFI firmware support.
  5. dnsmasq: Used by default NAT for DHCP/DNS.

Start the service:

Host:

1
2
sudo systemctl enable --now libvirtd
systemctl status libvirtd

Add the current user to the libvirt group:

Host:

1
sudo usermod -aG libvirt $(whoami)

Takes effect after re-login.

Installer Options That Matter Later

During the installation process, at minimum confirm:

  1. IPv4 is enabled.
  2. IPv6 is enabled.
  3. Select OpenSSH server installation.
  4. Network mode is libvirt default NAT.

If OpenSSH server isn’t installed initially, you’ll only be able to type commands in the VM console, which is very inefficient.

Missing Tools Are Normal After a Lean Setup

After installation, seeing the following status is no cause for alarm — this is by design:

  1. No ping.
  2. No vim.
  3. No nano.
  4. No net-tools.
  5. Usually only ip is available.

So the initial troubleshooting core will be ip addr, ip route, /proc/net/route, /etc/resolv.conf.

Symptom: apt update Fails

VM:

1
sudo apt update

Typical error:

1
Temporary failure resolving 'archive.ubuntu.com'

This error looks like DNS on the surface, but in practice it’s often “routing + DNS both broken simultaneously.”

Diagnostic Checklist Before Making Changes

Identify the Real Interface Name

VM:

1
ip addr

Need to capture:

  1. The real interface name (e.g., enp0s1, ens1, enp1s0).
  2. Don’t assume it’s always eth0.

Ubuntu 24.04 naming may differ across virtualization platforms. Writing the wrong interface name in commands will invalidate all subsequent configuration.

Scope-Local v6 Addresses: Seeing inet6 Does Not Imply WAN Reachability

A common situation in ip addr is:

  1. Interface has inet6.
  2. But no usable inet (IPv4).

This usually means IPv4 DHCP didn’t get an address, leaving only IPv6 link-local. It’s a warning sign of “network not fully connected,” not a normal internet-accessible state.

Confirm the Fallback Route Exists

VM:

1
2
ip route
cat /proc/net/route

If /proc/net/route has almost nothing but headers, or ip route has no default via ..., it means the kernel doesn’t know where to send packets.

Inspect the Resolver Configuration

VM:

1
cat /etc/resolv.conf

If nameserver is empty, invalid, or points to an unreachable target, apt will get stuck at domain resolution.

Find the Hypervisor Bridge Address

Host:

1
ip addr

Find virbr* interfaces, usually virbr0, but names aren’t guaranteed to be fixed.

Then check its IPv4, for example:

1
inet 192.168.122.1/24

This address is the VM’s gateway candidate.

Check Packet Relaying and Masquerade Rules

Check bridge status:

Host:

1
ip addr show virbr0

Check IP forwarding:

Host:

1
sysctl net.ipv4.ip_forward

If it shows:

1
net.ipv4.ip_forward = 0

The VM almost certainly cannot reach the internet through the Host.

Check FORWARD chain:

Host:

1
sudo iptables -L FORWARD -n -v

Check if policy is DROP and whether virbr packet forwarding is allowed.

Check NAT POSTROUTING:

Host:

1
sudo iptables -t nat -L POSTROUTING -n -v

Need to see something like:

1
MASQUERADE  all  --  192.168.122.0/24  anywhere

Without MASQUERADE, even with a gateway, the VM often can’t reach the external network.

Virtualization-Server Fixes

Enable IP forwarding:

Host:

1
sudo sysctl -w net.ipv4.ip_forward=1

Set FORWARD policy to ACCEPT:

Host:

1
sudo iptables -P FORWARD ACCEPT

Add NAT rule:

Host:

1
sudo iptables -t nat -A POSTROUTING -s 192.168.122.0/24 -j MASQUERADE

Re-verify:

Host:

1
2
sudo iptables -L FORWARD -n -v
sudo iptables -t nat -L POSTROUTING -n -v

Guest-OS Manual Connectivity Procedure

First confirm two things:

  1. VM NIC name, e.g., enp1s0.
  2. Host virbr IP, e.g., 192.168.122.1.

Manually assign IP:

VM:

1
sudo ip addr add 192.168.122.100/24 dev enp1s0

Create same-subnet route:

VM:

1
sudo ip route add 192.168.122.0/24 dev enp1s0

Add default gateway:

VM:

1
sudo ip route add default via 192.168.122.1 dev enp1s0

Check routing:

VM:

1
ip route

Need to see default via 192.168.122.1 dev enp1s0.

Force-inject DNS:

VM:

1
echo "nameserver 8.8.8.8" | sudo tee /etc/resolv.conf

Connectivity test:

VM:

1
2
3
ping 8.8.8.8
ping archive.ubuntu.com
sudo apt update

Once this step works, subsequent tool and service installation can proceed.

Remote Shell for Better Ergonomics Than the Viewer

A common pain point is that the VM console can’t smoothly copy-paste, making long commands error-prone.

First install core tools:

VM:

1
sudo apt update && sudo apt install openssh-server vim

Start SSH:

VM:

1
sudo systemctl enable --now ssh

Connect from Host:

Host:

1
ssh user@192.168.122.100

After this, use your local terminal for VM operations — much more efficient.

Container Engine Setup: Official Source Instead of docker.io

Ubuntu’s built-in docker.io package works, but this time I used the Docker official source instead. Reasons:

  1. Version updates are usually faster.
  2. buildx and compose v2 plugin integration is more complete.
  3. Security update cadence stays in sync with official.

Incremental Container Runtime Installation

Create keyring directory:

VM:

1
sudo install -m 0755 -d /etc/apt/keyrings

Import Docker GPG:

VM:

1
2
3
curl -fsSL https://download.docker.com/linux/ubuntu/gpg | \
sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg
sudo chmod a+r /etc/apt/keyrings/docker.gpg

Add Docker repository:

VM:

1
2
3
4
echo "deb [arch=$(dpkg --print-architecture) signed-by=/etc/apt/keyrings/docker.gpg] \
https://download.docker.com/linux/ubuntu \
$(. /etc/os-release && echo "$VERSION_CODENAME") stable" \
| sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

Install Docker Engine and plugins:

VM:

1
sudo apt update && sudo apt install docker-ce docker-ce-cli containerd.io docker-buildx-plugin docker-compose-plugin

Set up sudo-less Docker (current user):

VM:

1
2
sudo usermod -aG docker $USER
newgrp docker

Verify:

VM:

1
docker ps

Validation Checklist at the End

Host-side:

  1. libvirtd is running normally.
  2. virbr* bridge exists with IPv4.
  3. net.ipv4.ip_forward = 1.
  4. FORWARD chain allows VM packet forwarding.
  5. NAT has MASQUERADE for the VM subnet.

VM-side:

  1. Interface has valid IPv4.
  2. Routing table has default via <virbr IP>.
  3. /etc/resolv.conf has a usable nameserver.
  4. apt update succeeds.
  5. SSH login works.
  6. Docker hello-world runs successfully.

This completes the entire path from “Minimal initially unusable” to “operationally manageable, container-deployable.”

References

  1. libvirt: NAT forwarding (virtual networks)
  2. libvirt: Network XML format (including default NAT example)
  3. Ubuntu Server docs: Networking (How-to)
  4. Ubuntu Server docs: About Netplan
  5. Docker Docs: Install Docker Engine on Ubuntu
  6. Docker Docs: Linux post-installation steps