Pakkit.net
← Back to blog

Infrastructure

Lift-and-Shift Is a Risk Assessment, Not a Dockerfile

Containerizing an old application looks like writing a Dockerfile, but the real work is finding every assumption the app makes about its host — a hardware-bound license, inbound connections, files it rewrites itself — and proving the container won't break them.

  • Infrastructure
  • Docker
  • Legacy
  • Migration

Someone asks “can we just put this old app in a container?” and the honest answer is almost never “sure, here’s a Dockerfile.” I spent a while assessing whether a crusty, end-of-life Java server appliance could be lifted off its aging VM into a container, and the Dockerfile was the easy 10%. The other 90% was discovering every quiet assumption the application made about the machine it ran on — and deciding whether a container would honor those assumptions or silently break production. Lift-and-shift is a risk assessment that happens to end in a Dockerfile, not the other way around.

The app assumes things it never wrote down

A long-lived application accretes dependencies on its environment that nobody documented because, on its original host, they were just true. The appliance I looked at had a pile of these, and each one was a potential showstopper:

  • A hardware-bound license. It validated its license against a network interface’s MAC address. A container gets a different MAC, so the license would read as invalid, and the app would quietly drop to a crippled trial mode and eventually shut itself off. The fix exists (pin the container’s MAC), but you only look for it if you know the dependency is there.
  • Inbound connections and a dynamic port range. Other systems connected to it, and it allocated ephemeral ports across a wide range for sessions. Default bridge/NAT container networking mangles source addresses and makes that range awkward, so it needed host-level or MAC-VLAN networking to preserve reachability and identity.
  • Its entire state was a local directory. No external database — the system of record was an in-memory cache flushed to disk. Lose that directory and you lose everything, so it had to be a carefully preserved volume.
  • It rewrote its own files. On certain events it modified files inside its own install directory, so a read-only image layer would break it.
  • Graceful shutdown was load-bearing. A clean stop flushed state to disk; a hard kill could corrupt it. So the container had to run the process as PID 1 with real signal handling and a generous stop grace period.

None of that is in a Dockerfile tutorial. All of it is the actual job.

The Dockerfile is where the work ends. The work itself is finding everything the app believes about its host.

A container is a new house, not a renovation

Here’s the reframe that kept me honest: containerizing the app is moving it to a new house, not renovating it. Every assumption it made about the old house — the wiring, the plumbing, where the doors are — has to be reproduced or consciously changed. The app doesn’t get better in the move; it just lives somewhere new, and your job is to make the new place close enough to the old one that it doesn’t notice.

That framing tells you what to inventory before you write any config: identity (MAC, hostname, IP), network shape (who connects to whom, in which direction, on what ports), persistent state (what must survive a restart), writable paths, and lifecycle (how it starts and, crucially, how it must stop). Walk each one and ask “does a container preserve this, and if not, what’s the mitigation?”

Rehosting is not remediation

The most important sentence in the whole assessment was this: containerizing an end-of-life app does not fix anything about it. The ancient runtime, the unpatched libraries, the missing transport security, the fact that the vendor no longer exists — all of that travels with the app into the container, unchanged. A container is a hosting decision, not a security or modernization decision.

This matters because “we containerized it” can create a false sense that the problem was addressed. It wasn’t. If the real problem is an unsupportable, unpatchable application, moving it into a container just gives the same liability a newer-looking home. The durable fix is still to replace or retire it; the container is, at best, a way to get it off a dying OS while you do that. Be clear with everyone about which problem you’re solving.

Prove it on a copy before you touch production

Because the failure modes are subtle — a license that silently degrades, a flush that doesn’t happen on a hard kill, a source IP that gets rewritten — the only honest validation is to stand the container up against a copy of real state and check the assumptions one by one, before anything near production. Confirm the license accepts, the inbound connections actually arrive, the dynamic ports work, a graceful stop flushes cleanly and a restart reloads the state. And keep the old VM intact and powered off as an instant rollback, because the first real cutover is where the assumption you missed announces itself.

The takeaway

When someone frames a migration as “just containerize it,” that framing is the risk. The container is trivial; the assumptions are everything. Treat a lift-and-shift as an investigation into what the app needs from its world, write down each dependency and its mitigation, and be explicit that rehosting an unsupported app rehosts its problems too. It’s the same spirit as rehearsing containers-versus-VMs decisions deliberately and remembering that golden images have to forget their old host. If you’ve lifted a stubborn legacy app into a container, I’d like to hear what assumption bit you.