Legacy application rescue
The situation
The client's internal business application had been running continuously since approximately 2012. It was written in PHP 5 and used the mysql_* extension family — the procedural MySQL API that was deprecated in PHP 5.5 and removed entirely in PHP 7.0. The application handled real business operations and was used by staff daily. Nobody described it as optional.
The developers who built it had left years ago. There was no active maintenance. There was no documentation worth the name. Nobody currently employed at the client had ever looked at the source code in a professional capacity, and nobody had any interest in doing so — the application worked, it ran in the background, and the institutional knowledge of what it did and how it did it existed only in the memory of people who no longer worked there.
What forced the issue wasn't a planned migration or a strategic decision. The server's hard drive had begun logging write errors. The hardware was failing. This was no longer a theoretical future problem — it was an active countdown. The only installation media was a collection of old CDs. The original server hardware it had been installed on no longer existed. There was no path to reinstallation. If the running server died before the application was moved, the application was gone.
The drive was already throwing write errors when the engagement started. The question wasn't whether to migrate — it was whether the migration could be completed before the hardware made the decision for everyone.
Why rewriting wasn't the answer
The reflex answer to "legacy application running on outdated software" is usually "rewrite it." It's the answer vendors prefer, it's what consultants without the specific skills to do otherwise will recommend, and it has the appeal of producing something modern and maintainable. It is also, in this situation, the wrong answer.
Rewriting requires understanding what the application does — not just superficially, but completely. Every edge case, every business rule baked into the code, every quirk of the data model that developed over ten years of production use. The people who had that knowledge were gone. A rewrite would take months, cost significantly, and carry a real risk of producing something that didn't behave the same way in cases the development team didn't know to test for. And it would still need to be migrated to — which is its own project.
The goal was to keep a working system working, move it off hardware that could fail at any moment, and make it safe, maintainable, and recoverable. Not to build something new.
Why an iocage jail was the right tool
FreeBSD iocage jails provide OS-level containerization — an isolated environment that shares the host kernel but has its own filesystem, network stack, and process space. The critical property for this engagement was that a jail can run a software environment that the host OS doesn't need to support. The host runs FreeBSD 14.3. The jail runs whatever the application needs — including PHP 5, including the mysql_* extension, including the specific versions of every dependency the application was built against.
The jail is isolated from the host and from every other service on the server. A problem inside the jail — a crash, a security issue, a misbehaving process — cannot affect the host OS or adjacent services. The blast radius of any failure is bounded by the jail boundary.
Critically, a jail is a directory on the host filesystem. That means it inherits ZFS snapshot semantics. Before every step of the migration, a snapshot. If something went wrong, rollback was a single command. The entire migration was low-risk not because the work was simple but because every step was reversible.
The migration process
The running server was imaged before anything else. A complete, consistent snapshot of the live filesystem — application code, configuration, database contents, everything — before any changes were made. This was the safety net: if the migration failed in any way, the original system was still intact and running.
A new iocage jail was created on the FreeBSD 14.3 host. Inside the jail, a PHP 5 environment was constructed that matched the original server's configuration — the same PHP version, the same mysql_* extension, the same web server configuration, the same supporting libraries. This was done from the image of the original system, not from memory or documentation, which meant the environment was derived from what was actually running rather than what anyone thought was running.
The mysql_* extension required specific attention. It was removed from PHP in version 7.0 and is not available in any standard package repository for a current FreeBSD system. It was built from source inside the jail — isolated from the host, with no impact on anything outside the jail boundary.
Application code and data were migrated into the jail. The application was started and verified against the original — same inputs, same outputs, same behavior. No code changes were made. The application had no awareness that it was running inside a jail on a server several major OS versions newer than the one it was built for.
ZFS snapshots were taken at each verification checkpoint. At any point during the process, the jail could have been rolled back to a previous state or the original server could have been brought back as the primary. The migration had no irreversible steps until the final cutover, which was itself a single, testable change.
With the application running correctly, the jail's network access was locked down using the host's pf firewall. The jail was given access only to the specific hosts and services it legitimately needed to function — its database, its upstream dependencies, the interfaces used by the application's users. Everything else was denied by default. An application that was previously running with no network boundary now had an explicit, auditable one.
The preserved PHP 5 jail solved the immediate crisis. But it was also an opportunity to give the client something they didn't have before: a controlled environment in which to modernize the application on their own schedule, without risk to the production system.
A second iocage jail was provisioned on the same host running PHP 7, accessible from the production jail's network. A separate subdomain was configured pointing to this development environment, giving newly hired developers a live target to port the application against — running the same data, the same supporting services, but against a modern PHP stack. The two jails could communicate where needed, making it possible to run the legacy and modern versions side by side during the porting process and validate behavior against real production data before any cutover.
The client did eventually complete the port and upgrade. But they did it properly — with time, with a real development environment, and with the production system still running reliably underneath them the entire time. The jail migration bought them that time. Without it, the options were rewrite under pressure or lose the application entirely.
What changed — and what didn't
The application didn't change. The code is the same code that's been running since 2012. The database schema is unchanged. The behavior that users depend on is identical. From the application's perspective, nothing happened.
From an infrastructure perspective, everything changed. The application now runs inside an isolated jail on current hardware. It is snapshotted by ZFS on a regular schedule — point-in-time recovery is available for any snapshot retained. The jail can be cloned to a new host in under an hour: zfs send | zfs receive across a network connection, start the jail, verify, done. The single point of failure — that one aging physical server with no recovery path — no longer exists.
The application is now visible to monitoring. Process health, service availability, and resource consumption are all observable in ways they weren't on the original server. Problems surface as alerts rather than as user complaints.
And critically: the environment is now reproducible. The jail is a defined, versioned thing that can be documented, backed up, and rebuilt. The knowledge of what the application needs to run is no longer locked in the configuration of an undocumented physical server.
The bhyve alternative — when jails aren't enough
The jail approach works when an application's dependencies can be satisfied within a single OS. For systems too tightly coupled to their original kernel or hardware environment for containerization — older FreeBSD versions with specific kernel ABI requirements, applications with unusual device dependencies, systems whose configuration is entangled with the base OS in ways that can't be cleanly separated — the same outcome is achievable via bhyve virtualization.
In the bhyve path, the entire original OS environment is migrated into a virtual machine running on the FreeBSD 14.x host. The guest retains its original OS, filesystem layout, and application stack intact. From the outside, nothing changes. From the inside, the application moved from aging physical hardware to a modern hypervisor with full ZFS snapshot support, hardware independence, and the same clonability and recovery properties as the jail approach.
We have used this path for a FreeBSD 11.2 production system where the application's dependencies made containerization inadvisable. The REST API it served continued responding without interruption throughout the migration.
Application changes
Zero. Same code, same database, same behavior. PHP 5 and mysql_* preserved intact.
Risk eliminated
Failing hardware with no reinstall path replaced by current server with full ZFS snapshot recovery.
Recovery capability
ZFS point-in-time snapshots. Clone to new host in under an hour via zfs send | zfs receive.
Security posture
pf-isolated to minimum required network access. Host OS unaffected by anything inside the jail.
Path forward
PHP 7 dev jail + subdomain gave developers a safe modernization target. Client completed the port on their own timeline.
The lesson
A preserved legacy application inside an iocage jail is not technical debt you are kicking down the road. It is a managed, isolated, reproducible environment with a defined boundary — and it can be delivered under pressure, on failing hardware, in a timeframe that a rewrite cannot match. The client's drive was already throwing write errors when the engagement started. A rewrite would have taken months. The jail migration took days and left the production system running throughout.
The second lesson is about the path forward. A jail migration doesn't lock you into the legacy stack — it buys you time to leave it properly. A second jail with a modern PHP environment, a subdomain, and access to real production data gave the development team a safe place to port the application without any pressure on the live system. The client eventually completed the upgrade. They did it on their own schedule, with a real development environment, against real data, with the production application running reliably underneath them the entire time. That's the outcome a forced rewrite under hardware failure pressure almost never produces.
Remote-first. Dallas-based. Available until 2am CT.