Many businesses depend on stable, revenue-generating web applications that were written 5 to 10 years ago. These systems work well, but they often run on an ageing virtual private server (VPS) running an unsupported Linux distribution, where no one dares to touch the environment for fear of breaking fragile runtime dependencies.
Rewriting the entire application is usually cost-prohibitive and introduces massive risk.
A far more pragmatic first step is containerising the existing application with Docker. Here is our step-by-step strategy for doing so cleanly.
The Goal: Reproducibility and Isolation
The goal of containerising legacy applications is not to turn them into microservices. The goal is to pin down runtime dependencies (PHP/Ruby/Node versions, system libraries, configuration files) so the application can run anywhere reliably.
┌──────────────────────────────────────────────┐
│ Host OS (Modern Linux: Debian/Ubuntu LTS) │
│ │
│ ┌──────────────────────────────────────┐ │
│ │ Reverse Proxy (Nginx + Let's Encrypt)│ │
│ └──────────────────┬───────────────────┘ │
│ │ │
│ Docker Internal Network │
│ │ │
│ ┌──────────────────▼───────────────────┐ │
│ │ Legacy App Container │ │
│ │ (Exact legacy runtime & dependencies)│ │
│ └──────────────────┬───────────────────┘ │
│ │ │
│ ┌──────────────────▼───────────────────┐ │
│ │ Dedicated Database Container │ │
│ │ (Isolated storage volume) │ │
│ └──────────────────────────────────────┘ │
└──────────────────────────────────────────────┘
Step 1: Pin Exact Legacy Runtimes
Rather than upgrading language runtimes immediately, match the current production version inside a Dockerfile.
For example, if your legacy service runs on an older PHP 7.4 stack:
FROM php:7.4-fpm-alpine
# Install exact legacy dependencies
RUN apk add --no-cache libpng-dev libzip-dev \
&& docker-php-ext-install pdo_mysql gd zip
WORKDIR /var/www/html
COPY . .
# Enforce secure file permissions
RUN chown -R www-data:www-data /var/www/html
By freezing the exact language and library version in the container, you eliminate “works on my machine” issues and ensure the application boots identically on any modern host.
Step 2: Extract Configuration to Environment Variables
Legacy apps frequently hardcode database credentials and API keys in configuration files like config.php or settings.py.
Refactor these files to read from system environment variables:
// Before: Hardcoded credentials in source control
$dbHost = '127.0.0.1';
$dbUser = 'root';
$dbPass = 'secret123';
// After: Environment variables with safe fallbacks
$dbHost = getenv('DB_HOST') ?: 'database';
$dbUser = getenv('DB_USER') ?: 'app_user';
$dbPass = getenv('DB_PASSWORD') ?: '';
This allows you to store sensitive credentials safely in a .env file on the host machine rather than checking them into version control.
Step 3: Decouple Persistent Storage
Containers must be disposable. If a container crashes or restarts, user uploads and session state should never be lost.
Map all directories where files are written to explicit Docker volumes:
services:
app:
build: .
volumes:
- uploads_data:/var/www/html/public/uploads
environment:
- DB_HOST=db
- DB_NAME=app_production
db:
image: mariadb:10.5
volumes:
- db_data:/var/lib/mysql
environment:
- MYSQL_DATABASE=app_production
- MYSQL_USER=app_user
volumes:
uploads_data:
db_data:
Now, updating the application image can be done without touching the database or uploaded files.
Summary
Containerisation buys legacy applications a new lease on life. You get:
- Rapid disaster recovery (re-spawning on a new host takes minutes instead of days).
- Automated backup routines targeting isolated storage volumes.
- A safe foundation from which you can incrementally modernise code components over time.