ИТ развој

Dockerizing Legacy PHP: A Pragmatic Path to Modernization

Докеризирање на застарен PHP: прагматичен пат кон модернизација

Legacy PHP rarely fails because the language is inherently unmanageable. It fails because its runtime has become invisible: a hand-configured server, an aging extension, a cron job nobody owns, and deployment steps remembered by one person. Docker does not rewrite that history. It gives the application a stable boundary around it, which is often the safest first move toward modernization.

The practical goal is not to make an old codebase look fashionable. It is to make its behavior repeatable, understandable, and safer to change.

Start by preserving reality

A legacy application may depend on a specific PHP version, Apache module, image library, database driver, timezone setting, or writable directory. Treat those dependencies as requirements to discover, not flaws to erase on day one.

Before choosing a base image, inventory what the running system actually needs:

  • The PHP version and enabled extensions.
  • The web server and document root.
  • Required PHP configuration values, especially upload limits, error logging, sessions, and timezone.
  • Writable paths for uploads, cache files, logs, and generated reports.
  • External services such as MySQL, Redis, SMTP, object storage, and scheduled jobs.
  • Environment-specific configuration currently embedded in files or server settings.

This inventory prevents a common mistake: building a clean-looking container that quietly omits a critical extension or points Apache at the wrong directory. A container that cannot reproduce production behavior is not modernization; it is a new outage mechanism.

Build a small, explicit runtime

For an application served through Apache, the official PHP Apache image can make the first container straightforward. Pin a concrete PHP release that is compatible with the application and its dependencies. Avoid using an unqualified latest tag: repeatability is the point.

FROM php:8.2-apache

RUN docker-php-ext-install pdo_mysql

RUN a2enmod rewrite

COPY . /var/www/html/

RUN chown -R www-data:www-data /var/www/html/var

EXPOSE 80

This example is intentionally modest. It installs the PDO MySQL extension, enables Apache rewrite rules, copies the application, and grants the web-server user access to a known writable directory. In a real project, the correct PHP version and extensions must come from the dependency inventory, not from preference.

Do not broadly make the whole application writable. Identify the directories that truly need write access and keep source code read-only where possible. That distinction improves both debugging and security: unexpected writes become easier to spot.

Make the document root explicit

Many older frameworks expose only a public or web directory. If Apache serves the repository root, configuration files and internal code may become reachable. Set the document root deliberately and ensure rewrite rules route requests to the front controller when the application needs one.

Likewise, do not assume an existing .htaccess file is active. Apache must allow overrides for the relevant directory, or the rules must be moved into the virtual-host configuration. Test ordinary routes, missing pages, uploaded files, and static assets after changing this boundary.

Separate application configuration from the image

Images should contain code and runtime dependencies. Environment-specific values should arrive at runtime through environment variables, mounted configuration, or a secret-management system appropriate to the deployment environment.

A development-oriented Compose file can make service dependencies visible:

services:
  app:
    build: .
    ports:
      - "8080:80"
    environment:
      APP_ENV: development
      DB_HOST: db
      DB_NAME: legacy_app
      DB_USER: legacy_user
      DB_PASSWORD: change-me
    depends_on:
      - db

  db:
    image: mysql:8.0
    environment:
      MYSQL_DATABASE: legacy_app
      MYSQL_USER: legacy_user
      MYSQL_PASSWORD: change-me
      MYSQL_ROOT_PASSWORD: root-change-me
    volumes:
      - db_data:/var/lib/mysql

volumes:
  db_data:

This is useful for local development, but it is not a production secret strategy. Do not bake credentials into the image, commit real passwords, or assume depends_on means the database is ready to accept connections. It controls startup order, not application-level readiness.

Legacy applications often attempt a database connection immediately and fail if MySQL is still initializing. The robust fix is application-level retry behavior with a bounded delay and useful logging. If changing the application is not yet feasible, ensure the deployment platform has health checks and restart behavior that matches the application’s failure mode. The important point is to make startup behavior intentional rather than lucky.

Handle state without pretending it does not exist

Containers are disposable. Legacy PHP systems frequently are not. They may write uploads to disk, use file sessions, generate thumbnails, or append logs locally. Dockerizing the application exposes this state, which is valuable because hidden state is one of the hardest obstacles to reliable deployment.

Classify each writable path:

  • Uploads and user-generated files: use durable storage, such as a managed volume or object storage, with a backup and recovery plan.
  • Cache and temporary files: keep them disposable when possible and ensure the application can rebuild them.
  • Logs: prefer standard output and standard error so the runtime platform can collect them consistently.
  • Sessions: file sessions may work for one instance, but shared session storage is needed before scaling across multiple application containers.

Do not mount the entire source tree into a production container merely to preserve uploads or permit hot fixes. That blurs the deployed artifact and makes rollbacks unreliable. Mount only the data paths that require persistence.

Use containerization to create a testable deployment contract

A Dockerfile becomes a concise statement of how the application runs. That makes it an excellent place to add verification. Build the image in continuous integration, run it, and exercise a small set of high-value checks: a health endpoint, a representative page, a database connection path, and a migration or schema validation step where applicable.

Database migrations deserve particular caution. A migration may be safe to run once but unsafe to run concurrently from multiple web containers. Make one deployment component responsible for migrations, confirm that the migration mechanism is idempotent or guarded, and keep a rollback plan that recognizes database changes are often harder to reverse than application code.

Also separate web requests from background work. A cron task running inside an Apache container can work temporarily, but it couples scheduling to web-server lifecycle. Define scheduled tasks as distinct jobs or services when the deployment environment supports it. The same principle applies to queue workers: they should have their own process, logs, restart policy, and resource limits.

Modernize in layers, not in a single leap

Docker is a platform for gradual improvement. Once the application runs predictably, the next changes become less risky: add automated tests around critical behavior, move credentials out of files, standardize logs, introduce health checks, update one dependency at a time, and eventually upgrade PHP.

Do not combine all of those changes in the first containerization effort. A PHP upgrade can reveal deprecated behavior; a database upgrade can change SQL modes or authentication; a framework upgrade can alter routing and sessions. When everything changes at once, failures become difficult to diagnose. A stable container around the existing behavior gives each later improvement a clear baseline.

The container is the beginning of operational clarity

The best outcome is not a repository with a Dockerfile. It is an application whose runtime assumptions are visible, whose dependencies can be recreated, and whose deployment can be tested before it reaches users.

That is why Dockerizing legacy PHP is so valuable. It turns accumulated operational folklore into versioned configuration. The code may still be old, but it is no longer trapped in an old way of running—and that is often the most pragmatic modernization step a team can take.

Портрет на автор на блогот

Mihajlo

Јас сум Михајло - развивач поттикнат од љубопитност, дисциплина и постојаната желба да создадам нешто значајно. Споделувам увиди, упатства и бесплатни услуги за да им помогнам на другите да ја поедностават својата работа и да растат во постојано развивачкиот свет на софтверот и вештачката интелигенција.