Back to Blog
Tutorial9 min read2026-05-01

Docker Migration Guide: Containerizing Your Legacy Stack

A hands-on tutorial for containerizing a multi-service legacy stack — web server, background workers, and scheduled tasks — using Docker and Docker Compose.

Docker Migration Guide: Containerizing Your Legacy Stack

Most legacy stacks aren't a single app — they're a constellation of services: a web frontend, an API backend, a background worker process, scheduled scripts, and one or more databases. Containerizing each component individually and orchestrating them with Docker Compose (for local development) and a cloud PaaS (for production) is how you modernize without a full rewrite. This tutorial covers the complete process.

Anatomy of a Typical Legacy Stack

The stack we'll containerize:

  • Nginx — reverse proxy and static file serving
  • Node.js API — REST backend
  • Python worker — background job processor (queue consumer)
  • Cron scripts — scheduled data processing
  • PostgreSQL — primary database
  • Redis — session store and job queue

Step 1: Containerize the Node.js API

# api/Dockerfile
FROM node:20-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci

FROM node:20-alpine AS production
WORKDIR /app
COPY --from=builder /app/node_modules ./node_modules
COPY src/ ./src/
COPY package*.json ./

RUN addgroup -S api && adduser -S api -G api
USER api

ENV NODE_ENV=production
EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=10s CMD   wget -qO- http://localhost:3000/health || exit 1
CMD ["node", "src/server.js"]
# Test the API container
docker build -t my-api:dev ./api
docker run -p 3000:3000   -e DATABASE_URL=postgresql://user:pass@host:5432/db   -e REDIS_URL=redis://localhost:6379   my-api:dev

curl http://localhost:3000/health

Step 2: Containerize the Python Worker

# worker/Dockerfile
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 worker/ ./worker/
COPY shared/ ./shared/

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

CMD ["python", "-m", "worker.main"]

Step 3: Handle Cron Scripts as Separate Containers

Legacy cron scripts running on the host become dedicated cronjob containers on the cloud. First, make each script self-contained:

# cronjobs/Dockerfile
FROM python:3.12-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY scripts/ ./scripts/
RUN useradd -m cronuser && chown -R cronuser /app
USER cronuser
# No CMD — the schedule determines which script runs
# Test a specific script locally
docker build -t my-cronjobs:dev ./cronjobs
docker run --env-file .env.test my-cronjobs:dev python scripts/daily-report.py

Step 4: Write docker-compose.yml for Local Development

version: '3.9'

services:
  postgres:
    image: postgres:16-alpine
    environment:
      POSTGRES_DB: myapp
      POSTGRES_USER: dev
      POSTGRES_PASSWORD: devpassword
    volumes:
      - postgres-data:/var/lib/postgresql/data
    healthcheck:
      test: ["CMD-SHELL", "pg_isready -U dev -d myapp"]
      interval: 10s
      timeout: 5s
      retries: 5

  redis:
    image: redis:7-alpine
    healthcheck:
      test: ["CMD", "redis-cli", "ping"]
      interval: 10s
      timeout: 5s
      retries: 5

  api:
    build:
      context: ./api
      target: production
    ports:
      - "3000:3000"
    environment:
      DATABASE_URL: postgresql://dev:devpassword@postgres:5432/myapp
      REDIS_URL: redis://redis:6379
      NODE_ENV: development
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

  worker:
    build: ./worker
    environment:
      DATABASE_URL: postgresql://dev:devpassword@postgres:5432/myapp
      REDIS_URL: redis://redis:6379
    depends_on:
      postgres:
        condition: service_healthy
      redis:
        condition: service_healthy

volumes:
  postgres-data:
# Start the full stack locally
docker compose up --build

# Run database migrations
docker compose exec api npx sequelize-cli db:migrate

# Verify everything is healthy
docker compose ps
docker compose logs api --tail=20

Step 5: Push Images to GitHub Container Registry

# Log in to GHCR
echo $GITHUB_TOKEN | docker login ghcr.io -u YOUR_GITHUB_USERNAME --password-stdin

# Build and tag
docker build -t ghcr.io/your-org/my-api:latest ./api
docker build -t ghcr.io/your-org/my-worker:latest ./worker
docker build -t ghcr.io/your-org/my-cronjobs:latest ./cronjobs

# Push
docker push ghcr.io/your-org/my-api:latest
docker push ghcr.io/your-org/my-worker:latest
docker push ghcr.io/your-org/my-cronjobs:latest

Step 6: Deploy Each Component on PandaStack

npm install -g @pandastack/cli
panda login

# Deploy the API container (auto-deploys on GitHub push)
panda deploy --name my-api --image ghcr.io/your-org/my-api:latest

# Deploy the worker as a background container
panda deploy --name my-worker   --image ghcr.io/your-org/my-worker:latest   --type worker

# Create cronjobs for each scheduled script
panda cronjob create   --name daily-report   --image ghcr.io/your-org/my-cronjobs:latest   --schedule "0 6 * * *"   --command "python scripts/daily-report.py"

panda cronjob create   --name hourly-cleanup   --image ghcr.io/your-org/my-cronjobs:latest   --schedule "0 * * * *"   --command "python scripts/cleanup.py"

Step 7: Run Migrations in Production

# Run migrations as a one-off container execution before going live
panda run   --image ghcr.io/your-org/my-api:latest   --command "npx sequelize-cli db:migrate"   --env DATABASE_URL="$PROD_DB_URL"

Containerizing a legacy stack is an iterative process — start with the web service, get it running in production, then migrate the worker, then the cron jobs. Each step is independently verifiable. By the end, your legacy stack is portable, reproducible, and deployable anywhere Docker runs. Visit [docs.pandastack.io](https://docs.pandastack.io) for PandaStack's full container deployment documentation.

Ready to deploy?

Start free on PandaStack — no credit card required.

Start free on PandaStack

More in Tutorial

Browse all Tutorial articles →

See also