September 14, 2025
Photo by Dean Pugh on Unsplash

Continuous Integration and Continuous Deployment (CI/CD) have become essential to streamline development, automate repetitive tasks, and improve code quality and delivery speed. In this blog post, we’ll explore setting up a CI/CD pipeline for a multi-module Java application using GitHub Actions, Docker, and Azure Container Apps. This guide covers the entire process — from building Docker images for each service to deploying them on Azure.

Prerequisites

Before we dive into the setup, ensure you have the following:

  • A multi-module Java project structured as microservices.
  • An Azure account with access to Azure Container Apps and Azure Container Registry.
  • A GitHub repository to host your project code.

Project Structure

In this example, we have a Java project with two services: service-hello and service-world. We aim to automate the process of building and pushing these services as Docker images and deploying them to Azure Container Apps whenever code is pushed to the main branch.

Architecture Design

Here’s what the project structure looks like:

multi-module-hello-world/
├── pom.xml               # Parent POM file
├── service-hello/
│   ├── pom.xml
│   ├── src/main/java/...
│   ├── Dockerfile
├── service-world/
│   ├── pom.xml
│   ├── src/main/java/...
│   ├── Dockerfile

Step 1: Creating the Dockerfile

Creating a Dockerfile for each of your Java microservices is essential for building the Docker images that will be deployed to Azure Container Apps.

We will create a dockerfile with multi-stage build process. The first stage uses Maven with OpenJDK 17 to compile the application and package it into an executable JAR. The second stage employs a slim OpenJDK image to run the application, copying the JAR file, exposing required service port, and defining the command to execute the application.

Dockerfile for service-hello

Create a file named Dockerfile inside the service-hello directory with the following content:

# Stage 1: Build the service-hello application
FROM maven:3.8.5-openjdk-17 AS builder
WORKDIR /app

# Copy the root pom.xml for dependency resolution and project structure
COPY ../pom.xml /app/pom.xml
COPY ../service-hello/pom.xml /app/service-hello/pom.xml

# Copy the source files of the entire project into the container
COPY .. /app

# Build the service-hello module specifically
RUN mvn clean package spring-boot:repackage

# Stage 2: Run the service-hello application
FROM openjdk:17-jdk-slim
WORKDIR /app

# Copy the JAR file from the builder stage
COPY --from=builder /app/service-hello/target/service-hello-1.0-SNAPSHOT.jar app.jar

# Expose the port
EXPOSE 8080

# Run the application
ENTRYPOINT ["java", "-jar", "app.jar"]

Dockerfile for service-world

Create a file named Dockerfile inside the service-world directory with the following content:

# Stage 1: Build the service-world application
FROM maven:3.8.5-openjdk-17 AS builder
WORKDIR /app

# Copy the root pom.xml for dependency resolution and project structure
COPY ../pom.xml /app/pom.xml
COPY ../service-world/pom.xml /app/service-world/pom.xml

# Copy the source files of the entire project into the container
COPY .. /app

# Build the service-world module specifically
RUN mvn clean package spring-boot:repackage

# Stage 2: Run the service-world application
FROM openjdk:17-jdk-slim
WORKDIR /app

# Copy the JAR file from the builder stage
COPY --from=builder /app/service-world/target/service-world-1.0-SNAPSHOT.jar app.jar

# Expose the port
EXPOSE 8081

# Run the application
ENTRYPOINT ["java", "-jar", "app.jar"]

Step 2: Setting up the Azure Environment

  1. Create an Azure Resource Group in your chosen region.
Azure resources group

2. Create a azure container registry javamicroserviceapp

Azure Container Registry

NOTE: I have set the networking to Public access and Disabled the encryption.

Navigate to access keys, enable admin user and save the login server credentials for future usage.

Azure container registry access keys

3. Build and push docker images for each service

# Login to azure account
az login

# Azure container registry login
az acr login --name javamicroserviceapp

# Build the service-hello docker image
docker build -t service-hello:v1 -f service-hello/Dockerfile .

# Tag the service-hello docker image
docker tag service-hello:v1 javamicroserviceapp.azurecr.io/service-hello:v1

# Push the service-hello docker image
docker push javamicroserviceapp.azurecr.io/service-hello:v1

# Build the service-world docker image
docker build -t service-world:v1 -f service-world/Dockerfile .

# Tag the service-world docker image
docker tag service-world:v1 javamicroserviceapp.azurecr.io/service-world:v1

# Push the service-world docker image
docker push javamicroserviceapp.azurecr.io/service-world:v1
Azure Container Registry

4. Create azure container app for both these services.

Container app for service-hello
Container app for service-world

Navigate to application URL to check once both the services gets deployed.

Application URL output for service-hello and service-world

Step 3: Configure Azure and GitHub secrets

  1. Azure Service Principal: We’ll need an Azure service principal for GitHub Actions to authenticate with Azure.

Run the following command to create azure service principal

az ad sp create-for-rbac --name "github-actions" --role contributor --scopes /subscriptions/{subscription-id}/resourceGroups/{resource-group} --sdk-auth

Replace {subscription-id} and {resource-group} with your actual Azure subscription ID and resource group. This will output JSON credentials.

Azure service principal output

2. Store Credentials in GitHub Secrets:

  • Go to your GitHub repository.
  • Navigate to Settings > Secrets and variables > Actions.
  • Create the following secrets:

AZURE_CREDENTIALS – The service principal JSON output from the Azure CLI.

ACR_NAME – The name of your Azure Container Registry.

RESOURCE_GROUP – The Azure resource group for your Container Apps.

CONTAINER_REGISTRY_1 – The name of your 1st Azure Container Registry.

CONTAINER_REGISTRY_2 – The name of your 2nd Azure Container Registry.

CONTAINER_APP_ENVIRONMENT_2 – The name of your 2nd Azure Container App.

DOCKER_USERNAME and DOCKER_PASSWORD – For ACR, use your Azure login credentials.

GitHub repository secrets

Step 4: Create GitHub Actions Workflow

Next, let’s create a GitHub actions workflow in our repository at.github/workflows/deploy.yml.

name: Deploy to Azure Container Apps

on:
  push:
    branches:
      - main  # Trigger the workflow on push to the main branch

jobs:
  build-and-deploy:
    runs-on: ubuntu-latest

    steps:
      # Step 1: Checkout code with commit history
      - name: Checkout code
        uses: actions/checkout@v2
        with:
          fetch-depth: 2  # Fetch the last two commits for change detection

      # Step 2: Set Commit ID for Tagging
      - name: Set Commit ID as Tag
        run: echo "COMMIT_ID=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_ENV

      # Step 3: Check which services changed
      - name: Check changed files
        id: changes
        run: |
          if git diff --name-only HEAD^ HEAD | grep -q '^service-hello/'; then
            echo "HELLO_CHANGED=true" >> $GITHUB_ENV
          fi
          if git diff --name-only HEAD^ HEAD | grep -q '^service-world/'; then
            echo "WORLD_CHANGED=true" >> $GITHUB_ENV
          fi

      # Step 4: Login to Azure
      - name: Login to Azure
        uses: azure/login@v1
        with:
          creds: ${{ secrets.AZURE_CREDENTIALS }}

      # Step 5: Install Azure CLI containerapp extension
      - name: Install Azure CLI containerapp extension
        run: |
          az extension add --name containerapp --upgrade

      # Step 6: Login to Azure Container Registry (if any service changed)
      - name: Login to Azure Container Registry
        if: env.HELLO_CHANGED == 'true' || env.WORLD_CHANGED == 'true'
        run: |
          echo ${{ secrets.DOCKER_PASSWORD }} | docker login ${{ secrets.ACR_NAME }}.azurecr.io -u ${{ secrets.DOCKER_USERNAME }} --password-stdin

      # Step 7: Build and Push Docker Image for service-hello
      - name: Build and Push service-hello
        if: env.HELLO_CHANGED == 'true'
        run: |
          docker build -t ${{ secrets.ACR_NAME }}.azurecr.io/${{ secrets.CONTAINER_REGISTRY_1 }}:${{ env.COMMIT_ID }} -f ./service-hello/Dockerfile .
          docker push ${{ secrets.ACR_NAME }}.azurecr.io/${{ secrets.CONTAINER_REGISTRY_1 }}:${{ env.COMMIT_ID }}

      # Step 8: Build and Push Docker Image for service-world
      - name: Build and Push service-world
        if: env.WORLD_CHANGED == 'true'
        run: |
          docker build -t ${{ secrets.ACR_NAME }}.azurecr.io/${{ secrets.CONTAINER_REGISTRY_2 }}:${{ env.COMMIT_ID }} -f ./service-world/Dockerfile .
          docker push ${{ secrets.ACR_NAME }}.azurecr.io/${{ secrets.CONTAINER_REGISTRY_2 }}:${{ env.COMMIT_ID }}

      # Step 9: Deploy service-hello to Azure Container Apps
      - name: Deploy service-hello
        if: env.HELLO_CHANGED == 'true'
        run: |
          az containerapp update --name ${{ secrets.CONTAINER_APP_ENVIRONMENT_1 }} --resource-group ${{ secrets.RESOURCE_GROUP }} --image ${{ secrets.ACR_NAME }}.azurecr.io/${{ secrets.CONTAINER_REGISTRY_1 }}:${{ env.COMMIT_ID }}

      # Step 10: Deploy service-world to Azure Container Apps
      - name: Deploy service-world
        if: env.WORLD_CHANGED == 'true'
        run: |
          az containerapp update --name ${{ secrets.CONTAINER_APP_ENVIRONMENT_2 }} --resource-group ${{ secrets.RESOURCE_GROUP }} --image ${{ secrets.ACR_NAME }}.azurecr.io/${{ secrets.CONTAINER_REGISTRY_2 }}:${{ env.COMMIT_ID }}

This GitHub Actions workflow automates CI/CD for deploying two microservices, service-hello and service-world, to Azure Container Apps. It checks for changes in each service, builds and pushes updated Docker images to Azure Container Registry, and then deploys the relevant service to Azure Container Apps if changes are detected. The workflow also includes authentication steps for Azure and the registry, ensuring secure and efficient deployments.

Our workflow is now ready and it’s now time to test our entire setup 😥

Shanti hi toh nahi hai bhai mere jeevan mai 🤧

Step 5: Pushing Changes and Testing the Workflow

It’s time to verify our workflow and test the entire setup. Let’s push some changes into service-hello and test the deployment workflow.

As soon as you commit the changes you will notice that your workflow has been triggered.

GitHub actions workflow triggered

Now, let’s wait for few minutes till our workflow gets completed.

GitHub actions workflow successfully executed

Our workflow has been successfully executed and it built, pushed and deployed the service-hello on our azure environment.

Let’s verify the changes by navigating to azure container registry, container apps and application URLs.

New image deployed to container registry
New image deployed to service-hello container app
Service-hello application URL

Our entire setup and workflow is working smoothly and it now fully automated the deployment process for our Java microservice project 🎉.

Waqt lagta hai par end mai sab acha hi ho jata hai ❤️‍🩹

Conclusion

In this blog, we’ve built a robust CI/CD pipeline that leverages GitHub Actions to automate the deployment of a multi-module Java application to Azure Container Apps. This approach not only reduces manual intervention but also ensures that each microservice is independently deployable, enabling faster, more flexible updates.

I hope you found this content informative and enjoyable. Happy coding! 🚀

Thank you for reading! 💚

Leave a Reply

Your email address will not be published. Required fields are marked *