A Docker host often begins as one convenient VM. A compose file becomes five, volumes accumulate, ports collide, and eventually nobody knows whether the VM is one service or an entire platform. The answer is not automatically “one VM per container.” It is to redraw boundaries around failure, trust, data, and operations.
Inventory before moving #
Start from the running system, not only the compose files. List every stack, image source, published port, network, bind mount, named volume, secret, scheduled job, hardware device, and external dependency. Record which data is authoritative and which can be recreated.
Then capture behavior: startup order, health checks, backup duration, peak memory, CPU architecture, disk growth, and how clients find the service. A container may depend on a host-mounted certificate or an undocumented cron job that is invisible in its compose file.
Do not upgrade application versions during this discovery. A migration already changes storage, networking, and runtime placement. Combining it with a major application upgrade makes every failure ambiguous.
Choose boundaries by shared fate #
Services that must be backed up, restored, upgraded, and rolled back together are candidates for one stack and one guest. A web application and its dedicated database may belong together in a small VM if their recovery point must match. Several unrelated applications sharing only Docker do not.
Trust matters too. Internet-facing code should not automatically share a Docker socket, writable filesystem, or broad network with an administrative tool. Separating them into different guests provides a clearer kernel and firewall boundary.
Keep operational overhead proportional. Twenty tiny VMs mean twenty operating systems to patch and monitor. One enormous VM means every maintenance event has a large blast radius. Group services by lifecycle and risk, then stop when each group has an understandable owner and recovery procedure.
Decide between a VM and LXC #
Docker inside a conventional Linux VM is predictable: it owns its kernel-facing runtime environment, firewall behavior, and storage assumptions within the guest. The cost is additional memory and another kernel to maintain.
Running Docker inside an LXC container can be efficient, but it adds nested container concerns, feature requirements, and device or filesystem edge cases. Security and backup behavior depend on whether the LXC is privileged, which features are enabled, and how storage is mounted. Do not copy a collection of host overrides from a forum without understanding the isolation they weaken.
For a durable service, choose the environment the team can restore at 2 a.m. A modest VM is often worth the overhead. Use LXC for Docker only when its advantages are measured and the configuration is documented and tested.
Separate configuration, secrets, and data #
Compose files and application configuration belong in version control without secrets. Credentials belong in a secret store or protected deployment path. Persistent application data belongs on storage with a defined backup and restore method.
Named Docker volumes are easy to create and easy to forget. Map each volume to an application purpose and decide whether it needs a crash-consistent VM backup, an application-native export, or both. Databases should be quiesced or exported according to their own documentation; copying live data files is not a generic backup strategy.
Avoid placing essential data on an LXC mount point that the chosen Proxmox backup mode excludes. Verify actual backup content with a restore test rather than assuming a path is included.
Design the new network path #
Give each guest a stable internal name. Publish user-facing services through a reverse proxy and keep database ports private. Write the required flows between proxy, application, database, DNS, monitoring, backup, and administration.
This is a chance to remove accidental exposure. A port published on all Docker host interfaces may have been reachable across more VLANs than intended. Enforce policy at the guest or network firewall and verify both permitted and denied paths after migration.
Preserve the old application URL during the move. Changing placement, hostname, TLS, and user bookmarks at once adds unnecessary work. DNS or the reverse proxy can switch the backend after testing.
Build a repeatable target #
Create the guest from a current template, patch it, install the container runtime from a trusted source, and deploy configuration through the normal automation path. Record resource allocation and avoid copying the old VM's size without measurement.
Do not copy the entire old Docker data directory to “save time.” Runtime state, storage-driver internals, and stale networks travel with it. Recreate containers from declared configuration and migrate only identified persistent data.
Before importing data, start the application empty. This proves that images, configuration, ports, health checks, and dependencies are correct independently of the dataset.
Migrate data with a consistency plan #
Choose between an online migration and a maintenance window. Most small homelabs benefit from a clear outage: stop writes, take the final application export or volume snapshot, transfer it, verify checksums, import it, and start the new service.
For large datasets, pre-copy immutable data while the old service remains online, then synchronize the small delta during the outage. Never assume a file copy is consistent while the application is writing. Follow the application's backup and restore instructions.
Record the exact cutoff point. If the new service must be rolled back after it accepts writes, the old instance is now stale. Either reverse-sync safely, restore from the new authoritative data, or declare that rollback ends once writes begin.
Test before switching clients #
Place a temporary name in internal DNS or use a test client with an explicit resolver override. Check login, data counts, uploads, background jobs, outbound email, integrations, time zone, and backup. Compare application logs between old and new instances.
Test restart and guest reboot, not only the first container start. Verify that services wait for dependencies, volumes mount before Docker, and the reverse proxy handles the expected headers and WebSocket paths.
Measure resource use under a representative task. A migration that functions but constantly swaps or fills its disk is not complete.
Create an acceptance checklist before the final sync. Include the application version, expected record or file count, successful login, one read and one write operation, scheduled task state, integration response, certificate identity, backup result, restart result, and one denied network path. The checklist makes “it looks fine” a reproducible decision.
Invite actual users to test workflows that the operator may not know. A photo application can display thumbnails successfully while new uploads or background indexing fail. Preserve enough time to investigate instead of treating user validation as ceremony.
Cut over with a rollback boundary #
Lower DNS cache time in advance if DNS controls the switch, or update the reverse proxy backend in one validated configuration change. Stop the old service or block writes so two independent copies cannot diverge.
Monitor error rate, latency, storage, and logs closely during the first normal usage period. Keep the old guest powered off but intact until the backup of the new system has succeeded and been restored in a test.
Then remove obsolete DNS records, firewall exceptions, volumes, and monitoring targets deliberately. Do not delete the old environment merely because the new homepage loaded once.
Decommission in two stages. First remove the old service from normal operation while keeping its guest and data read-only. After an agreed observation period and a successful restore of the new backup, export final audit information, remove obsolete secrets, and delete retired data according to policy.
Update the inventory at the same time. It should identify the new guest, owner, data paths, proxy route, monitoring check, backup job, restore instructions, and dependency group. Otherwise the runtime boundaries improve while operational knowledge stays behind.
Use the migration to reduce uncertainty #
The valuable outcome is not a prettier Proxmox tree. It is a smaller set of well-defined service groups, each with declared configuration, known data, narrow network access, observable health, and a tested recovery path.
Docker remains a useful application runtime inside those boundaries. Proxmox provides the guest lifecycle and backup integration. Clear ownership between the layers prevents the next convenient container from quietly rebuilding the same monolith.
Write one rule for future additions: a new workload must declare its owner, data, network exposure, backup need, and lifecycle group before deployment. The rule is small enough to follow and strong enough to keep convenience from silently erasing the new boundaries. Review that declaration during every major update.