How to Containerize a Legacy Service Without Breaking Production

Legacy services are tricky to containerize for a reason that rarely shows up in blog posts: the service itself is usually not the problem — the assumptions baked in around it are. A service that has been running on bare metal for six years has accumulated undocumented dependencies. It expects a specific kernel version. It writes to a path that only exists because of a symlink set up in 2019 by someone who left the company. It reads from a shared NFS mount that nobody put in the config management system. When you lift that service into a container, you are not just changing packaging — you are surfacing every assumption that was never written down. That is where projects blow up, and that is why the pre-work matters more than the Dockerfile.

Before I write a single line of container configuration, I do a dependency capture that goes well beyond the obvious. Ports, environment variables, and secrets are table stakes — the things that trip teams are everything else. I want to know where the service writes logs and whether anything downstream reads from those paths. I want to know what it writes to disk and whether that state needs to persist across restarts. I want to know its startup order: does it need a database to be available before it binds its port, or does it tolerate a degraded start? What does a healthy instance look like, and what signals indicate a problem before a user notices one? How is it deployed today — is there a deployment mechanism that assumes the binary lives at a specific path? What service account permissions does it run under, and are any of those permissions granted at the OS level rather than the application level? What does the network path look like — does it reach back to other services by hostname, and if so, which DNS resolver does it use? Getting all of this written down before you start is the only way to know what you are actually migrating.

The migration itself should be staged, not a replacement. The pattern I rely on is parallel operation: run the containerized instance alongside the original, route a slice of real traffic to it, and compare outputs before you cut over. This sounds obvious but most teams skip it because it feels slow. It is not slow — it is the only way to discover that your containerized instance is silently dropping a class of requests that the original handled fine, or that your health check passes but the service is actually writing to a tmpfs mount that disappears on restart. You cannot find these things in staging. You find them with real traffic and real comparison. The cutover should be a deliberate, observable event — not a migration that happens to be complete when somebody notices the old instance is gone.

Rollback for a containerized migration looks different from rollback for a config change. If the original service is still running in parallel, rollback is just a traffic shift — you move requests back to the original instance and you have bought yourself time to diagnose. If you cut over completely before validating, rollback requires you to restart the original service from whatever state it was in at cutover time, which may include uncommitted state, open connections, or lock files that the containerized instance did not clean up properly. The teams that handle this well keep the original instance warm and monitor-able for a defined period after cutover — not because they expect to use it, but because having a known-good fallback changes how carefully you cut over in the first place.

Verification is not "the container starts and the health check passes." That bar is too low. Real verification means the service handles the full range of requests it handled before, at the same latency profile, with the same error rate, writing to the same downstream systems in the same format. It means the logs are going where they need to go. It means the persistent state survives a container restart. It means the monitoring signals that existed before the migration are still being emitted and still being received by the systems that consumed them. I have seen containerization projects declared complete while the service was silently failing to write audit logs — the container was healthy, the app was healthy, but a compliance requirement was broken and nobody noticed for weeks. Verification is a checklist derived from the dependency capture, not a gut feeling.

There is a deeper principle behind all of this. Modernization should not require blind faith. The teams that containerize legacy services without incidents are not the teams with the best Dockerfiles — they are the teams that built enough observability and fallback into the migration process that they could afford to be wrong about something and still recover cleanly. Every assumption you surface before the change is a potential incident you avoided. Every parallel-run hour is a blast radius you kept bounded. Every health signal you define before the change window opens is a rollback trigger you can actually act on. The container is the destination; the migration process is what keeps production safe while you get there.