Anasayfa / Software / How to Containerize a Legacy Application with Docker & Docker Compose

How to Containerize a Legacy Application with Docker & Docker Compose

docker compose

Legacy applications are the backbone of many enterprises, but they often run on outdated environments that are hard to scale, test, or move. Docker and Docker Compose give you a lightweight, reproducible way to wrap those old systems in containers, making deployment predictable and portable. In this guide we’ll walk through everything you need to know to containerize a typical monolithic legacy app—think a Java‑based web service with a Tomcat server and a MySQL database—using Docker and Docker‑Compose. By the end you’ll have a fully functional, version‑controlled container setup you can run locally or push to any container orchestrator.

What You’ll Need

  • A workstation running Linux, macOS, or Windows Subsystem for Linux.
  • Docker Engine (latest stable) and Docker Compose installed.
  • Source code of the legacy application (e.g., a Git checkout).
  • Basic knowledge of the app’s runtime requirements (Java version, environment variables, ports, etc.).
  • Access to a container registry (Docker Hub, GitHub Packages, or a private registry) for optional image publishing.

Step 1: Assess the Existing Environment

Before you write any Dockerfile, document the current runtime. Identify the operating system, required libraries, Java JDK/JRE version, web server (Tomcat, Jetty, etc.), and any external services such as databases or message queues. Run the app locally and capture the exact command used to start it—often a startup.sh script or a catalina.sh run call. Take note of environment variables (e.g., DB_HOST, APP_ENV) and configuration files that live outside the codebase (properties, XML). This “snapshot” will become the blueprint for your Docker image.

Step 2: Write a Minimal Dockerfile

Create a Dockerfile at the root of the project. Start from an official base image that matches the original OS and Java version. For a Java 8 app on Debian, you might use openjdk:8-jdk-slim. Here’s a simple example:

FROM openjdk:8-jdk-slim

# Install any OS‑level packages the app needs (e.g., unzip, curl)
RUN apt-get update && apt-get install -y 
    unzip 
    && rm -rf /var/lib/apt/lists/*

# Create a non‑root user for security
RUN groupadd -r appgroup && useradd -r -g appgroup appuser

# Set working directory
WORKDIR /opt/app

# Copy the compiled WAR or JAR into the image
COPY target/my‑legacy‑app.war ./app.war

# Expose the port the app listens on (Tomcat default 8080)
EXPOSE 8080

# Switch to non‑root user
USER appuser

# Define entrypoint – use Tomcat’s startup script or java -jar
ENTRYPOINT ["java","-jar","/opt/app/app.war"]

Adjust the COPY line to match your build artifact (JAR, WAR, or a directory). Keep the Dockerfile small; you can layer additional steps later if you discover missing dependencies.

Step 3: Build and Test the Image Locally

Run the build command from the project root:

docker build -t legacy-app:dev .

If the build succeeds, start a container to verify the app boots:

docker run -d -p 8080:8080 --name test‑legacy legacy-app:dev

Open http://localhost:8080 in a browser. If you see the old UI or API response, the container works. If not, inspect logs with docker logs test‑legacy and iterate on the Dockerfile—perhaps you missed a library or the entrypoint is wrong.

Step 4: Add Supporting Services with Docker Compose

Most legacy apps depend on a database. Docker Compose lets you define a multi‑container environment in a single docker‑compose.yml file. Create the file alongside the Dockerfile:

version: "3.9"
services:
  app:
    image: legacy-app:dev
    build: .
    ports:
      - "8080:8080"
    environment:
      - DB_HOST=db
      - DB_USER=legacy_user
      - DB_PASSWORD=secret
    depends_on:
      - db
  db:
    image: mysql:5.7
    restart: unless‑stopped
    environment:
      MYSQL_DATABASE=legacydb
      MYSQL_USER=legacy_user
      MYSQL_PASSWORD=secret
      MYSQL_ROOT_PASSWORD=rootsecret
    volumes:
      - db_data:/var/lib/mysql
volumes:
  db_data:

This file does three things:

  • Builds the app image from the Dockerfile.
  • Exposes port 8080 on the host.
  • Starts a MySQL 5.7 container, persisting data in a named volume.

Run the whole stack with:

docker compose up -d

After a few seconds, the app should connect to the db service using the DB_HOST=db variable. Verify connectivity by checking the app logs or hitting an endpoint that queries the database.

Step 5: Externalize Configuration

Hard‑coding values in the Dockerfile or docker‑compose.yml makes the image less reusable. Move configuration to .env files or Docker secrets for production. Example .env:

APP_ENV=production
DB_HOST=db
DB_USER=legacy_user
DB_PASSWORD=supersecret

Then reference those variables in the compose file without exposing them in source control:

environment:
  - DB_HOST=${DB_HOST}
  - DB_USER=${DB_USER}
  - DB_PASSWORD=${DB_PASSWORD}

For truly sensitive data, replace the environment block with Docker secrets (available in Swarm or Kubernetes) and mount them at runtime.

Step 6: Optimize for Production and CI/CD

Once the container works locally, prepare it for production pipelines:

  • Multi‑stage builds: Use a builder stage with Maven/Gradle to compile the app, then copy only the artifact into a lightweight runtime stage. This reduces image size dramatically.
FROM maven:3.8-openjdk-8 AS builder
WORKDIR /build
COPY pom.xml .
COPY src ./src
RUN mvn clean package -DskipTests

FROM openjdk:8-jre-slim
WORKDIR /opt/app
COPY --from=builder /build/target/my‑legacy‑app.war ./app.war
EXPOSE 8080
ENTRYPOINT ["java","-jar","app.war"]
  • Tagging strategy: Use semantic tags like legacy-app:1.0.0 and legacy-app:latest. Push to a registry with docker push yourrepo/legacy-app:1.0.0.
  • Healthchecks: Add a HEALTHCHECK instruction so orchestrators can detect a stuck container.
HEALTHCHECK --interval=30s --timeout=5s 
  CMD curl -f http://localhost:8080/health || exit 1

Integrate these steps into your CI pipeline (GitHub Actions, GitLab CI, etc.) so every commit builds, tests, and pushes the image automatically.

Common Mistakes to Avoid

Even experienced engineers slip up when containerizing old code. Here are the most frequent pitfalls:

  • Copying source instead of compiled artifacts: Including the entire source tree inflates the image and may expose credentials. Always copy only the built JAR/WAR.
  • Running containers as root: This is a security risk. Create a dedicated user in the Dockerfile and use USER before ENTRYPOINT.
  • Hard‑coding hostnames or IPs: Legacy apps often reference localhost for databases. Replace them with service names defined in Docker Compose.
  • Neglecting volume persistence: Forgetting to declare a volume for databases leads to data loss on container recreation.
  • Missing .dockerignore: Without it, you may unintentionally ship large build directories, .git folders, or local config files into the image.

Tips and Tricks

These extra tips can make your migration smoother:

  • Use docker compose logs -f to stream logs from all services during debugging.
  • Leverage docker compose exec app bash to get a shell inside the running container for quick inspection.
  • If the legacy app expects a specific file layout, mount a read‑only bind mount for static assets instead of copying them into the image.
  • When moving to Kubernetes, the same docker‑compose.yml can be converted with tools like kompose.
  • Enable Docker BuildKit (DOCKER_BUILDKIT=1) for faster, parallel builds and better caching.

Frequently Asked Questions

Do I need to modify the legacy code to run in Docker?

Ideally no. Docker aims to replicate the original environment. However, you may need to externalize hard‑coded paths or replace local file references with environment variables.

Can I run multiple legacy apps on the same host?

Absolutely. Docker isolates each app in its own network namespace. Just give each service a unique name and map ports that don’t conflict (e.g., 8081, 8082).

What if the legacy app uses an old database version not available as an official image?

You can either find a community‑maintained image, build your own from the vendor’s binaries, or run the database on the host and connect via the host’s IP. The latter reduces containerization benefits, so custom images are preferred.

Conclusion

Containerizing a legacy application doesn’t have to be a monumental rewrite. By carefully mapping the existing runtime, crafting a lean Dockerfile, and orchestrating dependencies with Docker Compose, you gain reproducibility, easier testing, and a clear path toward modern deployment platforms. Follow the steps, watch out for the common mistakes, and leverage the tips to keep your migration smooth. Your once‑stubborn monolith is now a portable, version‑controlled service ready for the cloud.

Photo by Johan Taljaard on Unsplash

Etiketlendi: