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.0andlegacy-app:latest. Push to a registry withdocker push yourrepo/legacy-app:1.0.0. - Healthchecks: Add a
HEALTHCHECKinstruction 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
USERbeforeENTRYPOINT. - Hard‑coding hostnames or IPs: Legacy apps often reference
localhostfor 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 -fto stream logs from all services during debugging. - Leverage
docker compose exec app bashto 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.ymlcan be converted with tools likekompose. - 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






