Dockerizing Legacy Applications

Eventually, you will need to transition your legacy application to a containerized environment.

Docker provides the necessary tools to facilitate a smooth and efficient conversion process.

In the past, applications were run on physical machines, where we meticulously managed each system on our network, even devising proper naming schemes for our servers. The advent of virtual machines marked a shift, dramatically increasing the number of servers we had to manage. We would create and dismantle them as needed. Containers took this concept further, significantly reducing the startup time from several seconds or more for virtual machines to almost instantaneously for containers.

Essentially, a container is a well-isolated process that shares the same kernel as other processes on the machine. While several container technologies are available, Docker is the most popular. Docker’s brilliance lies in its ease of use and smooth operation, which quickly gained widespread adoption. It simplifies the complexity of starting a container and makes common operations as straightforward as possible.

While most modern applications are designed with containerization in mind, many legacy applications based on older architectures are still widely used. If your legacy application is running smoothly in its current setup, you might question the need for containerization.

The primary benefit of containers is the consistency of environments. Containerization ensures that an application runs uniformly across various environments by packaging the app and its dependencies together. This means the development environment on a developer’s laptop mirrors the testing and production environments. Such uniformity can lead to significant savings in testing and troubleshooting future releases. Additionally, containers offer horizontal scalability; you can scale the application by adjusting the number of containers.

By adding a container orchestration tool like Kubernetes, you can optimize resource allocation and maximize the use of your physical or virtual machines. Container orchestration simplifies scaling the application with the load, enabling efficient scaling, especially crucial for applications facing sudden load spikes. The rapid startup time of containers compared to virtual machines allows for quick scaling and has several other advantages. You can deploy applications faster and roll them back just as quickly if issues arise.

Convert your legacy application to a containerized environment.

Getting Started

To work with Docker, you need to set up a development environment. First, you’ll need to install Docker itself. Installation steps vary, depending on your operating system.

For this tutorial, we will use Ubuntu 24.04 as our operating system. Ubuntu is a popular choice for web hosting due to its stability, user-friendly interface, and extensive community support.

(Note, the Docker install for ARM servers is slightly different.  You need to use this GPT Key:

echo "deb [arch=arm64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null )

Installing Docker

    1. SSH to your vps and update your existing list of packages:

sudo apt update
sudo apt upgrade -y

2. Install required packages for Docker:

sudo apt install apt-transport-https ca-certificates curl software-properties-common -y

3. Add Docker’s official GPG key:

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /usr/share/keyrings/docker-archive-keyring.gpg

4. Add the Docker APT repository:

echo "deb [arch=amd64 signed-by=/usr/share/keyrings/docker-archive-keyring.gpg] https://download.docker.com/linux/ubuntu $(lsb_release -cs) stable" | sudo tee /etc/apt/sources.list.d/docker.list > /dev/null

5. Update the package database with Docker packages from the newly added repository:

sudo apt update

6. Install Docker:

sudo apt install docker-ce -y

Installing Docker Compose

    1. Download the current stable release of Docker Compose

sudo curl -L "https://github.com/docker/compose/releases/download/1.29.2/docker-compose-$(uname -s)-$(uname -m)" -o /usr/local/bin/docker-compose

2. Apply executable permissions to the binary:

sudo chmod +x /usr/local/bin/docker-compose

3. Verify that the installation was successful:

docker-compose --version

Convert your legacy application to a containerized environment.

Preparing for migration.

Evaluating the Current System and Its Requirements

Before starting the migration process, it’s crucial to evaluate your current system and its requirements. This involves understanding the architecture of your application, its dependencies, and its interactions with other systems. Additionally, you need to consider business requirements such as performance, availability, and security. This evaluation helps identify potential issues that may arise during the migration and allows for proper planning to address them.

Identifying Applications Suitable for Containerization

Not all applications are ideal for containerization. Legacy applications tightly coupled with the operating system or dependent on specific hardware may not be suitable candidates. Conversely, microservices or stateless applications designed to be distributed and scalable are perfect for containerization. It’s essential to distinguish which applications are suitable for containerization and which are not.

Defining a Container Strategy

After identifying suitable applications, the next step is to define a container strategy. This includes deciding how to use containers, selecting the type of container platform, managing and orchestrating containers, and addressing security and networking concerns. Your container strategy should align with your business objectives and consider the unique requirements of your applications.

Selecting a Container Platform

The final step in preparation for migration is choosing a container platform. There are numerous container platforms available, each with its strengths and weaknesses. Some platforms prioritize simplicity and ease of use, while others focus on scalability and enterprise-grade features. Your choice of platform should align with your needs and the requirements of your applications. Popular options include Docker, Kubernetes, and OpenShift.

The Migration.

The Migration Process: A Multi-Step Journey

Migrating applications is a multi-step project that requires planning, meticulous execution, and a deep understanding of both your existing applications and the target environment.

Disassemble Existing Applications

The first step in the migration process is breaking down, or disassembling, your existing applications. This step is crucial for understanding the components that make up your application and identifying any dependencies they may have. The goal here is to completely understand the building blocks of your application.

Think of disassemble your applications like disassembling a complex machine. Each component must be carefully cataloged and assessed for functionality and interdependencies. This process may involve reverse engineering, especially if the original design documentation is unavailable.

The goal at this stage is to create a comprehensive map of your application’s architecture, including data flows, interactions between components, and external dependencies. This map will be invaluable in later stages of the migration process.

Containerizing Each Component of the Application

After decomposing your applications into individual components, the next phase is containerizing each of these components. Containerization involves packaging each application component along with its dependencies into a container.

The process begins by creating a container image, a lightweight, standalone, executable package that includes everything needed to run a piece of software, such as the code, runtime, libraries, environment variables, and config files. Docker is the most common tool for creating container images, using a Dockerfile to define the image contents.

Once the container image is built, it can be run to create a container instance. Each container runs as an isolated process in user space on the host operating system. This isolation ensures that each containerized component can run without interfering with others, maintaining the application’s integrity and reliability.

During the containerization process, it’s crucial to maintain proper version control and use automated building processes. This helps ensure consistency and reliability across different stages of the application lifecycle, from development and testing to staging and production.

Testing Containerized Applications

After your applications have been containerized, the next step is testing. This critical phase ensures that your containerized applications function as expected in their new environment.

Testing involves validating that all components interact seamlessly, data flows correctly, and the application responds appropriately under varying load conditions. Automated testing tools can be invaluable during this stage, as they help quickly and accurately identify issues.

Deploying Containerized Applications

The final step in the migration process is deploying your containerized applications. Deployment involves placing your tested and validated containers into the production environment, ensuring a smooth transition and minimal disruption to users.

Example Migration: Legacy web app

Convert your legacy application to a containerized environment.

Dockerfiles

Dockerfiles are scripts used to build Docker images. Imagine these files as a set of instructions you would follow to set up your environment on a VPS after installing the host OS (like Ubuntu, CentOS, etc.). Docker images follow the same concept. With a Dockerfile, you create an image for your lightweight server, known as a container.

The Importance of Layer Order in a Dockerfile

The order in which instructions are written in a Dockerfile is crucial for several reasons:

1. Cache Efficiency

Docker uses a layer caching mechanism to speed up the build process. Each instruction in a Dockerfile creates a new layer in the image, and Docker caches these layers. If you modify a layer, Docker must rebuild that layer and all subsequent layers. Therefore, placing instructions that change less frequently earlier in the Dockerfile can significantly improve build times.

2. Dependency Management

The order of layers can affect how dependencies are managed and how errors are propagated. For example, installing base packages or dependencies early in the Dockerfile ensures they are available for later stages of the build process. This is crucial for maintaining a stable and functional build environment.

3. Build Performance

By ordering instructions logically, you can optimize the build performance. For instance, placing instructions that add or modify application code towards the end of the Dockerfile minimizes the number of layers that need to be rebuilt when the code changes. This results in faster iteration times during development.

4. Image Size

The size of the final Docker image can be affected by the order of the instructions. Combining related operations into single instructions and ordering them efficiently can help reduce the number of layers and the overall image size. This is important for reducing storage costs and improving the performance of pulling and deploying images.

Example

Here is an example to illustrate the importance of order in a Dockerfile:

# Start with the base image
FROM node:14
# Install dependencies
COPY package.json /app/
WORKDIR /app
RUN npm install
# Copy application code
COPY . /app
# Set environment variables
ENV NODE_ENV production
# Expose the port the app runs on
EXPOSE 3000
# Start the application
CMD ["node", "server.js"]

In this example:

  • Base Image: The base image is specified first, setting up the initial environment.
  • Dependencies: package.json is copied and npm install is run early to leverage layer caching. If the dependencies in package.json do not change, Docker can use the cached layer even if the application code changes.
  • Application Code: The application code is copied after installing dependencies. This way, changes to the application code do not invalidate the cached layers related to dependencies.
  • Environment Variables and Configuration: These are set towards the end, ensuring they are applied to the final stages of the build.
  • Expose and Command: These instructions come last to finalize the container configuration.

By understanding and utilizing the importance of layer order in a Dockerfile, you can create more efficient, maintainable, and performant Docker images.

The Dockerfile

FROM ubuntu:latest
ENV DEBIAN_FRONTEND=noninteractive
RUN apt-get update -y && \
apt-get upgrade -y && \
apt-get install -y apache2 php php-dev php-mysql libapache2-mod-php php-curl php-json php-common php-mbstring composer software-properties-common && \
a2enmod rewrite
COPY ./php.ini /etc/php/7.2/apache2/php.ini
COPY ./site.conf /etc/apache2/sites-available/site.conf
COPY ./apache2.conf /etc/apache2/apache2.conf
RUN rm -rfv /etc/apache2/sites-enabled/*.conf && \
ln -s /etc/apache2/sites-available/site.conf /etc/apache2/sites-enabled/site.conf
EXPOSE 80
EXPOSE 443
CMD ["apachectl", "-D", "FOREGROUND"]

Breakdown:

1. Base Image

FROM ubuntu:latest
  • Uses the latest version of Ubuntu as the base image.

2. Environment Variable

ENV DEBIAN_FRONTEND=noninteractive
  • Sets the DEBIAN_FRONTEND environment variable to noninteractive to suppress interactive prompts during package installations, which is required for automatic builds and prevents build failure.

3. Package Updates and Installations

RUN apt-get update -y && \
apt-get upgrade -y && \
apt-get install -y apache2 php php-dev php-mysql libapache2-mod-php php-curl php-json php-common php-mbstring composer software-properties-common && \
a2enmod rewrite
  • Updates the package list and upgrades installed packages.
  • Installs Apache, PHP, and a variety of PHP extensions.
  • The a2enmod rewrite command is used in Apache web server configurations to enable the mod_rewrite module.

4. Configuration Files

COPY ./php.ini /etc/php/7.2/apache2/php.ini
COPY ./site.conf /etc/apache2/sites-available/site.conf
COPY ./apache2.conf /etc/apache2/apache2.conf
  • Copies custom configuration files for PHP and Apache into the appropriate directories.

5. Apache Site Configuration

RUN rm -rfv /etc/apache2/sites-enabled/*.conf && \
ln -s /etc/apache2/sites-available/site.conf /etc/apache2/sites-enabled/site.conf
  • Removes existing site configuration files and creates a symbolic link to the custom site configuration file.

6. Expose required ports and starting Apache

EXPOSE 80
EXPOSE 443

CMD ["apachectl", "-D", "FOREGROUND"]

  • Exposes HTTP and HTTPS ports.
  • Defines the command to start Apache in the foreground when the container runs.

Building our Docker image

To build a Docker image from the Dockerfile, you can use two common methods: building with the Docker CLI and using Docker Compose.

Here is an explanation of both methods:

Method 1: Building with the Docker CLI

  1. Ensure Docker is Installed: Make sure Docker is installed on your system. You can verify this by running docker --version in your terminal.
  2. Navigate to the Directory: Open your terminal and navigate to the directory containing your Dockerfile using the cd command.
  3. Build the Docker Image: Use the docker build command to create the Docker image. You can specify a tag for your image with the -t flag.
docker build -t my-apache-php-image .

In this command:

  • -t my-apache-php-image tags the image with the name my-apache-php-image.
  • The . at the end specifies the current directory as the build context, which should contain the Dockerfile.

Method 2: Building with Docker Compose

  1. Ensure Docker and Docker Compose are Installed: Verify that both Docker and Docker Compose are installed on your system. You can check their versions using docker --version and docker-compose --version.
  2. Create a docker-compose.yml File: In the same directory as your Dockerfile, create a docker-compose.yml file with the following content:
  • In this file:
    • version: '3' specifies the version of the Docker Compose file format.
    • services defines the services that will run. In this case, there’s a single service named web.
    • build: . indicates that the Dockerfile in the current directory should be used to build the image.
    • ports maps port 80 and 443 on the host to the container.
  • Build and Run the Docker Image: Use the docker-compose up command to build the image and start the container:

docker-compose up --build

The --build flag ensures the image is built before starting the container.

Both methods will result in building a Docker image from the provided Dockerfile. The Docker CLI method (docker build) is straightforward and ideal for simple builds. Docker Compose is more suitable for multi-container applications or when you need to define complex configurations and dependencies. Choose the method that best fits your workflow and project requirements.