Skip to content

Chapter 6: Docker Container Packaging

Reyas Khan M edited this page Jun 5, 2025 · 1 revision

Welcome to Chapter 6! In the previous chapters, we met our three main microservices: the Shopfront, the Product Catalogue, and the Stock Manager. We also saw how they communicate with each other using REST APIs and Data Transfer Objects (DTOs) to share information like product details and stock levels.

Now that our services know how to talk, we face a new challenge: how do we actually run these services reliably? Each service is a Java application with its own dependencies (like Spring Boot or Dropwizard). Running them directly on a server would mean:

  • Installing Java on the server.
  • Making sure the correct Java version is used for each service (what if one needs Java 8 and another Java 11?).
  • Copying the compiled .jar files.
  • Installing any other necessary system libraries.
  • Configuring environment variables or configuration files correctly for each service.

Doing this manually for even just three services is tedious and error-prone. Imagine doing it for dozens or hundreds of microservices! Plus, what works on one developer's machine might not work on a testing server or in production due to small differences in the environment. This is often called "dependency hell" or "it works on my machine" syndrome.

This is where Docker comes to the rescue!

The Challenge: Inconsistent Environments

Imagine our microservices as different departments in a shop (Sales, Catalogue, Warehouse), each needing a specific set of tools and a dedicated workspace.

Running them traditionally is like trying to set up these workspaces manually in any available room. You need to bring the right tools, set up the furniture, make sure the temperature is right, etc., for each department, every time you move them to a new room. It's complicated and inconsistent.

The Solution: Docker as Standardized Packaging

Docker provides a way to package your application and everything it needs to run (code, runtime like Java, libraries, settings) into a single, standardized unit.

Think of it like putting each department into its own standardized, ready-to-ship box.

  • The box is self-contained; it has the department's tools and furniture already inside.
  • The box has clear instructions on how to set it up (just open the box and start!).
  • Any room (server) that can fit this standard box can run the department inside it, without needing special manual setup for that specific department.

This standardized box in Docker is called a Container.

Key Concepts in Docker Packaging

Let's break down the core ideas:

  1. Docker Image: This is like the blueprint or mold for your standardized box. It's a read-only template that contains your application code, the Java runtime, system libraries, and any configuration needed. You build an image once, and you can use it to create many identical containers.

    graph TD
        A[Your Application Code] --> B(Build Process)
        C[Required Software<br/>(Java, Libraries, etc.)] --> B
        D[Instructions<br/>(Dockerfile)] --> B
        B --> E(Docker Image)
        E --> F(Docker Registry)
    
    Loading

    You combine your code, necessary software, and instructions (Dockerfile) to build an image. You can then store this image in a Docker Registry (like Docker Hub) to share it.

  2. Docker Container: This is a running instance of a Docker Image. When you "run" a Docker Image, you create a container. It's the actual standardized box with your application running inside it. Containers are isolated from each other and from the host machine, ensuring consistency.

    graph TD
        E(Docker Image) --> G(Run)
        G --> H[Running Docker Container 1]
        G --> I[Running Docker Container 2]
        style H fill:#f9f,stroke:#333,stroke-width:2
        style I fill:#f9f,stroke:#333,stroke-width:2
    
    Loading

    You can take the same image and run it multiple times to create multiple identical containers.

  3. Dockerfile: This is the recipe or the set of instructions Docker uses to build an Image. It's a simple text file with commands like "start from a base operating system," "copy my application files," "install dependencies," "configure network ports," and "what command to run when the container starts."

    # This is a simple Dockerfile example
    FROM ubuntu:20.04 # Start from a base Ubuntu image
    COPY . /app       # Copy the current directory into /app in the image
    RUN apt-get update && apt-get install -y some-package # Run a command inside the image
    CMD ["python", "/app/hello.py"] # Command to run when a container starts

    Each line in the Dockerfile is a step in the image-building process.

Packaging Our Microservices with Docker

For each of our microservices, we have a Dockerfile that tells Docker how to build an image specifically for that service. Let's look at them.

Shopfront Microservice Dockerfile

We saw this briefly in Chapter 1. Here it is again:

# shopfront/Dockerfile
FROM openjdk:8-jre
ADD target/shopfront-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8010
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

Let's break down what each line does:

  • FROM openjdk:8-jre: This is the base layer for our image. We're starting with an official Docker image that already has Java 8 Runtime Environment (JRE) installed on a minimal Linux distribution. This means we don't have to install Java ourselves in the Dockerfile.
  • ADD target/shopfront-0.0.1-SNAPSHOT.jar app.jar: This command copies the application's executable JAR file (which we built using Maven, as described in Chapter 1) from our project's target directory into the Docker image. We rename it to app.jar inside the image for simplicity.
  • EXPOSE 8010: This line documents that the application inside the container intends to listen on port 8010. It's like putting a label on the box saying "This service uses port 8010." It doesn't actually make the port accessible from outside the container yet (we'll do that when we run or deploy the container). This matches the server.port=8010 setting we saw in the Shopfront's application.properties in Chapter 1.
  • ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]: This specifies the command that runs when a container is created from this image. It's essentially the command to start our Java application using the app.jar file we added. The -Djava.security.egd=... part is a common optimization for Java applications running in containers.

To build the image from this Dockerfile, you would typically use a command like docker build -t shopfront:latest . in your terminal from the shopfront directory.

Product Catalogue Microservice Dockerfile

Looking at the Dockerfile for the Product Catalogue service:

# productcatalogue/Dockerfile
FROM openjdk:8-jre
ADD target/productcatalogue-0.0.1-SNAPSHOT.jar app.jar
ADD product-catalogue.yml app-config.yml
EXPOSE 8020
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","app.jar", "server", "app-config.yml"]

This is very similar to the Shopfront, but with a few key differences due to it being a Dropwizard application:

  • FROM openjdk:8-jre: Same base Java image.
  • ADD target/productcatalogue-0.0.1-SNAPSHOT.jar app.jar: Copies the executable "fat" JAR (built by Maven's shade plugin as seen in Chapter 2).
  • ADD product-catalogue.yml app-config.yml: Adds the configuration file (product-catalogue.yml, which we saw in Chapter 2 setting the port 8020) into the image, renaming it app-config.yml.
  • EXPOSE 8020: Documents that the service listens on port 8020, matching the configuration file.
  • ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","app.jar", "server", "app-config.yml"]: The command to start the application. For Dropwizard, you typically run the JAR with server and the path to the config file as arguments.

This Dockerfile packages the Product Catalogue application and its configuration file into a single image.

Stock Manager Microservice Dockerfile

Finally, the Dockerfile for the Stock Manager:

# stockmanager/Dockerfile
FROM openjdk:8-jre
ADD target/stockmanager-0.0.1-SNAPSHOT.jar app.jar
EXPOSE 8030
ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]

This one is very similar to the Shopfront's, as it's also a Spring Boot application:

  • FROM openjdk:8-jre: Same base Java image.
  • ADD target/stockmanager-0.0.1-SNAPSHOT.jar app.jar: Copies the executable JAR (built by Maven's spring-boot plugin as seen in Chapter 3).
  • EXPOSE 8030: Documents the port 8030, matching its application.properties (seen in Chapter 3).
  • ENTRYPOINT ["java","-Djava.security.egd=file:/dev/./urandom","-jar","/app.jar"]: Command to run the JAR.

This Dockerfile packages the Stock Manager service into its own image.

The Build Process Illustrated

Putting it together, the process for each service looks something like this:

graph LR
    A[Microservice Code<br/>(e.g., Shopfront Java files)] --> B(Maven Build)
    C[Dependencies<br/>(Spring Boot, etc.)] --> B
    B --> D[Executable JAR file<br/>(target/*.jar)]
    E[Dockerfile] --> F(Docker Build)
    D --> F
    F --> G(Docker Image)
    G --> H(Run Command)
    H --> I[Running Docker Container]
Loading

You write your code, build an executable JAR using a tool like Maven. You write a Dockerfile defining the steps to create the container's environment. Then, the docker build command uses the Dockerfile and the JAR file to create a Docker Image. Finally, the docker run command creates a Docker Container from that image, and your application starts running inside the container.

Benefits Summary

By packaging each microservice into a Docker image using a Dockerfile, we achieve:

  • Portability: The image contains everything needed, so it can run on any machine with Docker installed, regardless of the host's operating system or installed software (beyond Docker itself).
  • Consistency: The application runs in the exact same environment every time, preventing "works on my machine" issues.
  • Isolation: Containers run in isolation from each other and the host system, avoiding conflicts between services (e.g., different services needing different versions of a library).
  • Simplified Deployment: Once built, deploying the application is just a matter of running the image as a container.

This makes our microservices much easier to build, share, and run consistently across different environments, from a developer's laptop to a production server managed by Kubernetes.

Conclusion

In this chapter, we learned how Docker is used to package each of our microservices into a self-contained unit called a Docker Image, based on instructions in a Dockerfile. We saw how each service's Dockerfile defines the base environment (FROM), adds the compiled application code (ADD), documents the network port it uses (EXPOSE), and specifies the command to start the application (ENTRYPOINT). This process creates portable, consistent Containers that encapsulate everything needed to run our microservices, solving the problem of environment inconsistencies.

Now that we know how to package individual microservices into containers, the next step is to learn how to run multiple related containers together and manage their interaction, especially when they need to talk to each other. This is where Docker Compose comes in.

Next Chapter: Docker Compose


Doc by Reyas Khan. References: [1], [2], [3]