Virtual machine migrations often look successful after the first simple checks.

The VM boots.
The IP address responds.
SSH works.
DNS works.
The default gateway is correct.
The Docker container is running and even reports healthy.

And yet the application is not reachable.

This is exactly the kind of problem where it is easy to spend time on the wrong diagnostic layer.

In this case, an Ubuntu virtual server was moved from to Proxmox. After the migration, the network adapter name changed from the original eth0 to ens18. The static IP configuration was adjusted, and the operating-system network worked correctly.

But the Docker application still did not respond on the expected LAN ports.

The key lesson was simple:

A working VM IP address does not mean that the Docker runtime restored the container network correctly.

And the even more important one:

A healthy container does not mean that the service is reachable over the network.

The first suspect: eth0 changed to ens18

After moving a Linux VM between hypervisors, a network interface name change is common.

On Hyper-V, the system may use something like:

eth0

After booting on , the interface may appear as:

ens18

That is not a problem by itself. It is a consequence of different virtual hardware and predictable Linux network-interface naming.

The correct first step is to verify the operating-system network:

ip -br link
ip -br addr
ip route
resolvectl status
networkctl status ens18

In this case, everything was correct:

  • ens18 was active,
  • the VM had the correct static IP address,
  • the default gateway worked,
  • DNS resolution worked,
  • SSH access was available,
  • the interface state was routable/online.

At this stage, it is important not to change Netplan, the gateway or DNS blindly just because the application still does not respond.

If the VM network itself works, the problem may be one layer higher.

The misleading symptom: the container is healthy

Docker showed that the application container was running.

A typical status looks reassuring:

container running
health: healthy
restart policy active

That can easily lead to the conclusion that the application is fine and that the problem must be in Linux networking, firewalling or Proxmox.

But Docker health checks are an internal view. Their meaning depends on how they are defined.

A health check may verify the service inside the container. It can succeed even when the container is not correctly attached to the Docker bridge network or when its ports are not published on the host.

That is why it is not enough to look only at:

docker ps

You also need to verify Docker runtime networking:

docker port <container>
docker inspect <container>
docker network inspect <network>

In this incident, the container was running and healthy, but Docker had not restored its network state correctly.

The real cause: the container lost Docker runtime networking

The problem was not the change from eth0 to ens18.

The problem was not Ubuntu static IP configuration either.

The real issue was Docker runtime state after the migration and restart.

The container started automatically, but it was not correctly attached to its original Docker bridge network. At the same time, it had no active port mappings.

The diagnostics showed something like this:

docker port <container>

returned no published ports.

And docker inspect showed empty or incorrect runtime sections such as:

NetworkSettings.Networks
NetworkSettings.Ports
HostConfig.PortBindings

That means the application could work inside the container, but the expected path did not exist:

VM LAN IP → published host port → Docker bridge → container

The result was this combination:

VM IP works
SSH works
container is running/healthy
application does not respond on the LAN port
docker port is empty
Docker bridge network does not have the container attached

This is exactly the kind of state where almost everything looks correct, but one runtime layer is broken.

Diagnostics must follow the layers

For problems like this, the worst approach is to make random changes.

The correct approach is to move layer by layer.

1. Verify the operating-system network

First, confirm that the Linux VM network is correct:

ip -br link
ip -br addr
ip route
resolvectl status
networkctl status ens18

Check:

  • the active interface name,
  • IP address and mask,
  • default gateway,
  • DNS,
  • routing,
  • interface state.

If this works and SSH is available, Netplan is probably not the root cause.

2. Verify Docker runtime, not only container status

Then move to Docker:

docker ps
docker port <container>
docker inspect <container>
docker network inspect <network>

The important fields are:

NetworkSettings.Networks
NetworkSettings.Ports
HostConfig.PortBindings

If the container is healthy but has no network attachment or published ports, the issue is in the Docker runtime layer.

The important distinction is this:

healthy inside container

is not the same as:

reachable from LAN

3. Verify host ports on the correct IP address

Another common mistake is testing only 127.0.0.1.

If a service is published only on a specific LAN IP address, it may not respond on loopback. That is not necessarily an error.

Test the real VM LAN IP:

curl -fsS http://<LAN-IP>:15000/health
curl -fsS http://<LAN-IP>:21121/api/health
ss -lntup

If ss does not show a listener on the expected IP and port, the problem is not the client. The host is not publishing the service as expected.

Safe repair: recreate only the affected service

If the Compose files, runtime .env file and Docker volumes are preserved, the safest fix is not to reinstall Docker or change the entire VM network.

The safer step is to recreate only the affected service.

Before doing that, check:

  1. that the Compose files exist;
  2. that the runtime env file exists;
  3. that the env file does not contain placeholder values;
  4. which named volumes the container uses;
  5. that you are not restarting or deleting the entire stack without a reason.

A controlled recreate may look like this:

docker compose \
  -p <compose-project> \
  -f <base-compose.yml> \
  -f <override-compose.yml> \
  --env-file <runtime.env> \
  up -d --force-recreate --no-deps <service>

Two options matter here:

--force-recreate

creates a new container with a correct Docker network sandbox.

--no-deps

limits the change to the specific service and avoids restarting dependencies unnecessarily.

If the application uses named volumes, the data remains preserved. That is important for databases, application keys and persistent runtime state.

Avoid this unless you fully understand the impact:

docker compose down -v

The -v flag may delete persistent volumes. On a production or important VM, that is a dangerous operation if you do not know exactly what will be removed.

Verification after repair

After the recreate, it is not enough to see that the container is running again.

Verify every layer:

docker inspect <container>
docker port <container>
docker ps
docker network inspect <network>
curl -fsS http://<LAN-IP>:<dashboard-port>/health
curl -fsS http://<LAN-IP>:<api-port>/api/health
ss -lntup

The repair is complete only when all of this is true:

  • the container is running,
  • the health check is healthy,
  • the container has an IP address in the Docker bridge network,
  • docker port shows the expected host bindings,
  • the host listens on the intended LAN IP and ports,
  • health endpoints respond through the LAN,
  • the service survives the next restart according to its restart policy.

Only then is the Docker application actually available, not merely alive internally.

Why this problem is easy to misdiagnose

This incident is deceptive because the first checks look good.

At the VM level:

IP works
gateway works
DNS works
SSH works

At the Docker level:

container running
container healthy

But application reachability still fails.

That creates the false impression that the problem must be in Proxmox, hypervisor bridge configuration, Linux routing or Netplan.

In reality, the VM network can be completely correct while Docker has failed to restore port publishing and bridge membership for one container after the migration.

That is why it is important to separate four different states:

VM network connectivity
Docker container process state
Docker network attachment and port publishing
Application reachability from LAN

Each of them can be correct or broken independently.

Preventive checklist before VM migration

Before moving a Linux VM with Docker applications, save the current state:

ip addr
ip route
docker ps
docker inspect <container>
docker network ls
docker network inspect <network>
docker volume ls
docker compose ls

For an important service, also export the relevant outputs:

docker port <container>
docker inspect <container> > container-before-migration.json
docker network inspect <network> > network-before-migration.json

After the migration, you have something concrete to compare against.

Preventive checklist after the first boot on Proxmox

After the first boot on Proxmox, do not stop at ping or SSH.

Verify:

ip -br addr
ip route
docker ps
docker port <container>
docker network inspect <network>
ss -lntup
curl -fsS http://<LAN-IP>:<port>/health

The service should be tested from the LAN, not only locally on the host.

If the VM network is correct, do not change Netplan blindly. Check Docker networking first.

Main lesson

During a virtual-server migration, the operating system may correctly move to a new adapter name and have working IP connectivity, while a Docker container remains detached from its bridge network or loses published ports after restart.

That means it is not enough to verify that the VM pings and the container is healthy.

The diagnostic path must follow the layers:

VM network

Docker runtime state

Docker bridge membership

Port publishing

Application health endpoint over LAN

Only when all those layers are correct can we say that the service migration succeeded.

The most important sentence from the whole incident is this:

A healthy container is not proof that the service is reachable over the network.

For production migrations, that is the difference between a server that merely looks started and a service that actually works.