Back to Blog
Tutorial8 min read2026-05-01

How to Containerize a Legacy Application with Docker

A practical tutorial for wrapping a legacy application in a Docker container so it can be deployed on any modern cloud platform.

How to Containerize a Legacy Application with Docker

Legacy applications — the ones running on bare metal servers, old VM images, or aging shared hosting accounts — are a fact of life for most engineering teams. Containerizing them with Docker is the first step toward modernization: it makes the app portable, reproducible, and deployable on any modern cloud PaaS. This tutorial walks through the process from initial audit to a working Dockerfile.

What "Legacy" Usually Means

Legacy apps tend to share a few characteristics:

  • Hardcoded file paths or configuration
  • Runtime dependencies installed globally on the host
  • No separation between application code and uploaded files
  • Direct database connections without connection pooling
  • Session state stored on disk rather than in Redis

None of these are blockers for containerization — they just need to be addressed systematically.

Step 1: Audit Your Runtime Dependencies

Before writing a Dockerfile, understand exactly what the application needs:

# On the legacy server — capture installed packages
dpkg --get-selections > installed-packages.txt

# For Python apps — capture exact versions
pip freeze > requirements.txt

# For Node.js apps
npm list --depth=0 > node-deps.txt

# Check what processes are actually running at runtime
ps aux | grep -E "(node|python|php|ruby|java)"

# Find config files your app reads
strace -e openat -p $(pgrep your-app) 2>&1 | grep -v ENOENT | head -50

Step 2: Identify External Dependencies

# Check what the app connects to
ss -tnp | grep your-app-pid

# Review database connection strings
grep -r "DB_HOST|DATABASE_URL|REDIS_URL" /path/to/app --include="*.env" --include="*.conf"

# Find filesystem dependencies (uploads, caches, logs)
lsof -p $(pgrep your-app) | grep REG | awk '{print $9}' | sort -u

Document every external port, file path, and environment variable. These become your Docker environment variables and volume mounts.

Step 3: Write Your First Dockerfile

Start with the closest official base image for your runtime. Don't try to replicate the legacy OS exactly — use a modern, minimal image.

For a Node.js 14 app (common legacy version):

FROM node:20-alpine

# Install any native dependencies your packages need
RUN apk add --no-cache python3 make g++

WORKDIR /app

# Copy dependency files first (layer caching)
COPY package*.json ./
RUN npm ci --only=production

# Copy application code
COPY . .

# Never run as root
RUN addgroup -S appgroup && adduser -S appuser -G appgroup
USER appuser

EXPOSE 3000
CMD ["node", "server.js"]

For a Python Flask app:

FROM python:3.12-slim

RUN apt-get update && apt-get install -y --no-install-recommends     gcc libpq-dev     && rm -rf /var/lib/apt/lists/*

WORKDIR /app

COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt

COPY . .

RUN useradd -m appuser && chown -R appuser /app
USER appuser

EXPOSE 8000
CMD ["gunicorn", "app:app", "--bind", "0.0.0.0:8000", "--workers", "2"]

Step 4: Handle Configuration Without Hardcoding

Legacy apps often have configuration baked into files. Externalize it:

# Find hardcoded config
grep -rn "localhost|127.0.0.1|/var/www|/home/deploy" /path/to/app   --include="*.js" --include="*.py" --include="*.php"

Replace each hardcoded value with an environment variable read at startup. For Node.js:

const dbHost = process.env.DB_HOST || 'localhost';
const uploadDir = process.env.UPLOAD_DIR || '/app/uploads';

Step 5: Handle File Uploads and Persistent Storage

Containers are ephemeral — anything written to the container filesystem is lost on restart. Move file uploads to object storage (S3, GCS) or mount a persistent volume.

For local testing, use a named volume:

docker run -v app-uploads:/app/uploads my-legacy-app

Step 6: Build, Test, and Iterate

# Build the image
docker build -t my-legacy-app:v1 .

# Run with a test environment file
docker run -p 3000:3000   --env-file .env.test   --name legacy-test   my-legacy-app:v1

# Tail the logs
docker logs -f legacy-test

# Run your smoke tests against the container
curl http://localhost:3000/health
curl -X POST http://localhost:3000/api/users   -H "Content-Type: application/json"   -d '{"email": "test@example.com"}'

# Clean up
docker stop legacy-test && docker rm legacy-test

Step 7: Push to GitHub and Deploy on PandaStack

Once the container passes local tests, push to GitHub and connect to PandaStack:

# Push your Dockerfile and code to GitHub
git add Dockerfile .dockerignore
git commit -m "Add Docker container configuration"
git push origin main

# Install CLI and deploy
npm install -g @pandastack/cli
panda login
panda init

PandaStack automatically builds and deploys your container on every push to your configured GitHub branch. Set your environment variables in the dashboard at [dashboard.pandastack.io](https://dashboard.pandastack.io) and you're live.

Containerization is the gateway to everything else: autoscaling, zero-downtime deploys, multiple environments, and modern CI/CD. The investment pays off immediately — and your future self will thank you for making the legacy app portable. See [docs.pandastack.io](https://docs.pandastack.io) for deployment configuration options.

Ready to deploy?

Start free on PandaStack — no credit card required.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also