CI/CD for Java with Azure DevOps & Docker

Tired of manual deployments and slow development cycles? This tutorial is your key to streamlined Java development! We’ll guide you through setting up an automated CI/CD pipeline in Azure DevOps. Leveraging Docker containers and Git for version control, you’ll be building and deploying your Java applications with ease, freeing you to focus on what matters most – crafting awesome code!

docker logo

1. Introduction

Developing Java applications can be a rewarding experience, but traditional workflows often involve repetitive and time-consuming tasks. Manually deploying code updates can be prone to errors, and slow development cycles can hinder your ability to innovate quickly. Studies by Park et al. (2019) have shown that slow deployments can lead to decreased developer productivity and higher customer churn.

This is where the concept of Continuous Integration and Continuous Delivery (CI/CD) comes in. CI/CD automates the development lifecycle, streamlining the process from code commit to production deployment. With CI/CD, every change to your codebase triggers a series of automated tasks, ensuring consistent builds, early detection of bugs, and faster deployments.

In this tutorial, we’ll leverage the power of Azure DevOps, Docker containers, and Git version control to create a robust CI/CD pipeline for your Java applications. Here’s how these tools work together:

  • Azure DevOps: As a cloud-based platform, Azure DevOps provides a central hub for managing your entire development process. It offers built-in tools for building, testing, and deploying applications, allowing you to define automated workflows through pipelines.
  • Docker Containers: Docker containers package your application with all its dependencies into a lightweight, self-contained unit. This eliminates compatibility issues across different environments and ensures consistent deployments regardless of the underlying infrastructure.
  • Git Version Control: Git acts as the central repository for your codebase, allowing developers to track changes, collaborate effectively, and revert to previous versions if necessary. The integration of Git with your CI/CD pipeline enables automatic builds and deployments triggered by code commits.

By combining these technologies, we can achieve a seamless development workflow for your Java applications. You’ll no longer need to worry about manual deployments or inconsistencies between development and production environments. This not only saves you valuable time but also leads to higher quality software and faster time-to-market for your projects.

2. Prerequisites

3. Steps

3.1 Create an Azure Container Registry (ACR)

Now that we understand the benefits of CI/CD for Java development with Docker, let’s set up the foundation – the Azure Container Registry (ACR). ACR acts as your secure, private repository specifically designed for storing Docker container images. Think of it as a dedicated library for all your Dockerized applications, accessible only by authorized users.

Here’s how to create your ACR in the Azure portal:

  1. Navigate to Azure Portal: Log in to your Azure account and access the Azure portal.
  2. Create a Resource Group (Optional): For better organization, it’s recommended to create a resource group specifically for your CI/CD pipeline resources. A resource group is a logical container that holds related Azure services. In the portal search bar, type “Resource group” and select the service. Click “Create” and provide a name for your resource group (e.g., “java-ci-cd-rg”).
  3. Search for Container Registry: In the search bar, type “Container registry” and select the service. Click “Create” to initiate the ACR creation process.
  4. Configure ACR Settings:
    • Name: Provide a unique name for your ACR instance that adheres to Azure’s naming conventions (5-50 alphanumeric characters, lowercase letters, dashes, numbers).
    • Subscription: Select the Azure subscription associated with your account.
    • Resource Group: If you created a resource group in step 2, choose it here. Otherwise, leave the default selection.
    • Location: Choose a geographic location closest to your target deployment environment for optimal performance.
  5. Select Pricing Tier: ACR offers various pricing tiers based on storage capacity, throughput, and geo-replication needs. Here’s a breakdown of some common options:
    • Basic: This free tier is suitable for development and testing purposes, offering 10 GB storage and limited throughput.
    • Standard: Ideal for most production deployments, it provides tiered storage options (starting from 100 GB) and higher throughput capacity.
    • Premium: This tier caters to high-performance scenarios with geo-replication capabilities for geographically distributed deployments.

For this tutorial, we’ll assume you’re setting up a development environment. So, feel free to choose the “Basic” tier to get started. Remember, you can always upgrade your ACR tier later based on your evolving needs.

  1. Review and Create: Once you’ve filled in the details, carefully review your configuration settings. When everything looks good, click “Create” to provision your Azure Container Registry.

After a short wait, your ACR instance will be ready to use. This secure repository will house the Docker images built during your CI/CD pipeline, ensuring they’re readily accessible for deployments.

3.2 Configure Docker Service Connection

Now that your Azure Container Registry (ACR) is up and running, it’s time to establish a secure connection between Azure DevOps and your private Docker image library. This connection, known as a service connection, allows your CI/CD pipeline to authenticate with ACR and seamlessly push the built Docker images to the registry.

Imagine your ACR as a secure vault guarded by access controls. A service connection acts like a digital key specifically authorized to enter the vault and deposit your Docker images. Without this key, your pipeline wouldn’t be able to interact with ACR and complete the deployment process.

Here’s how to create a service connection in Azure DevOps:

  1. Navigate to Project Settings: Within your Azure DevOps project, navigate to “Project settings” (usually found under the gear icon in the top navigation bar).
  2. Access Service Connections: Under “Project settings,” locate the “Service connections” section and click on it. This will display a list of existing service connections configured for your project.
  3. Create New Service Connection: Click the “+” button (typically labeled “New service connection”) to initiate the creation process.
  4. Select Service Connection Type: A list of available service connection types will appear. Scroll down and select “Docker registry” as the connection type.
  5. Configure Service Connection Details:
    • Connection Name: Provide a descriptive name for this service connection (e.g., “ACR-dev-registry”). This name will help you easily identify the connection within your pipelines.
    • Registry URL: Enter the login server address for your Azure Container Registry instance. You can find this URL on the overview page of your ACR in the Azure portal. It typically follows the format <your-acr-name>
    • Authentication Type: Leave the default selection as “Username/Password” for this tutorial. Azure DevOps also supports other authentication methods like Azure Active Directory (AAD) tokens, which can be explored for enhanced security in production environments.
    • Username: Enter the username associated with your ACR login credentials. This is typically your Azure subscription email address.
    • Password: Provide the password you created when setting up your ACR instance.
  6. Validate and Save: Once you’ve filled in all the details, click the “Save” button. Azure DevOps will attempt to validate the connection using the provided credentials. If everything is configured correctly, the connection will be established successfully.

By creating this service connection, you’ve essentially granted your Azure DevOps pipelines the necessary access to interact with your private Docker registry. This paves the way for the CI/CD pipeline to securely store and deploy your containerized Java applications.

3.3 Define the Build Pipeline (azure-pipelines.yml)

Now that we have the infrastructure in place (ACR and service connection), it’s time to orchestrate the magic – the CI/CD pipeline itself! Azure DevOps leverages YAML (YAML Ain’t Markup Language) as a human-readable format to define the stages and tasks involved in your pipeline. Think of YAML as a set of instructions that tell Azure DevOps what to do at each step of the build and deployment process.

Here’s a breakdown of a basic azure-pipelines.yml file outlining the core stages of our Java CI/CD pipeline:

# azure-pipelines.yml

  vmImage: ubuntu-latest  # Defines the virtual machine agent for pipeline execution

- checkout  # Stage 1: Fetch code from Git repository
- build     # Stage 2: Build the Java application
- docker    # Stage 3: Build and push Docker image

- job: Build_and_Deploy  # Defines a job within the pipeline stages

  - stage: checkout
    displayName: Checkout Code  # Friendly name for better readability
    script: git checkout .  # Command to fetch code from the Git repository

  - stage: build
    displayName: Build Java Application
      # Replace with your specific build command (e.g., mvn clean install or gradle build)
      mvn clean install

  - stage: docker
    displayName: Build and Push Docker Image
      # Login to ACR using the service connection (replace with your connection name)
      docker login --username $(ACR_USERNAME) --password $(ACR_PASSWORD) $acrLoginServer
      # Build the Docker image based on your Dockerfile (replace with your Dockerfile path)
      docker build -t $acrName:$imageTag .
      # Push the image to your ACR registry
      docker push $acrName:$imageTag

  # Environment variables can be defined outside the jobs section for reusability
    acrLoginServer: $(ACR_REGISTRY_URL)  # Login server address for ACR
    acrName: $(myacr)                   # Replace with your ACR name
    imageTag: $(Build.BuildNumber)                  # Use build number for image tagging


  • pool: This section specifies the virtual machine agent that will run your pipeline tasks. Here, we’re using the “ubuntu-latest” agent, suitable for most Java development scenarios.
  • stages: This defines the different stages involved in the pipeline. Each stage groups related tasks that execute sequentially. Our example showcases four stages:
    • checkout: Fetches the code from your Git repository.
    • build: Builds the Java application using your chosen build tool (Maven or Gradle).
    • docker: Builds a Docker image based on the Dockerfile in your project.
    • Push Image: Pushes the built Docker image to your ACR registry.
  • jobs: Jobs represent units of work within the pipeline stages. In this example, we have a single job named “Build_and_Deploy” that executes all the defined steps.
  • steps: Each stage consists of one or more steps, which are the specific commands or tasks executed by the agent. Our steps section demonstrates various functionalities:
    • Checking out code from the Git repository using the git checkout command.
    • Building the Java application using a placeholder script (replace this with your specific build command like mvn clean install or gradle build).
    • Logging in to ACR using the pre-defined service connection credentials stored as environment variables (ACR_USERNAME and ACR_PASSWORD).
    • Building the Docker image using the docker build command, referencing your Dockerfile path.
    • Pushing the built image to your ACR registry using the docker push command.
  • variables: This section defines environment variables used throughout the pipeline. Here, we’re defining variables for the ACR login server address, your ACR name, and the image tag (using the build number for versioning).

This YAML file provides a solid foundation for your CI/CD pipeline. Remember to replace the placeholder scripts and environment variables with your specific details. In the next sections, we’ll delve deeper into crafting a Dockerfile and exploring optional deployment stages.

3.4 Create a Dockerfile

Before we proceed, let’s take a moment to understand the magic behind Docker images. A Dockerfile acts as a blueprint or recipe that instructs Docker on how to assemble a custom image for your application. Imagine it as a set of instructions for building a Lego model – each line specifies a step in the creation process.

Here’s a code snippet for a basic Dockerfile tailored for your Java application:

# Dockerfile

FROM openjdk:17-slim  # Base image with Java 17 runtime

WORKDIR /app  # Working directory within the container

COPY . .  # Copy application code from build context to /app

EXPOSE 8080  # Expose the application port (typically 8080 for web applications)

CMD ["java", "-jar", "your-app.jar"]  # Command to start the Java application


  • FROM openjdk:17-slim: This line specifies the base image upon which our custom image will be built. We’re using the “openjdk:17-slim” image, which provides a lightweight Java 17 runtime environment. Remember to choose a base image compatible with your Java application’s requirements.
  • WORKDIR /app: This sets the working directory within the container to “/app”. This is where your application code will be copied and executed.
  • COPY . .: This command copies all files and directories from the current build context (your project directory) into the “/app” directory within the container. This ensures your application code is available for execution.
  • EXPOSE 8080: This line exposes port 8080 within the container. Typically, Java web applications run on port 8080, but adjust this if your application uses a different port.
  • CMD ["java", "-jar", "your-app.jar"]: This defines the default command to be executed when the container starts. Here, it instructs the container to run the Java application using the command java -jar your-app.jar. Replace “your-app.jar” with the actual name of your Java application JAR file.

This is a basic example. You might need to modify it based on your specific application’s needs. For instance, you might need to install additional dependencies or configure environment variables within the Dockerfile.

3.5 Configure Release Pipeline

While the core CI/CD pipeline focuses on building and storing Docker images, you can further enhance your workflow with automated deployments using Azure DevOps release pipelines. A release pipeline is a separate pipeline specifically designed to manage deployments to various environments (development, staging, production).

Here’s a simplified example of creating a release pipeline with a single stage that deploys your Docker image to an Azure environment:

  1. Create a New Release Pipeline: Within your Azure DevOps project, navigate to “Pipelines” and click on “Releases”. Select “New pipeline” to initiate the creation process.
  2. Choose a Template (Optional): Azure DevOps offers various pre-built templates for deploying to different environments. For deploying containerized applications, you might consider the “Azure App Service deployment” template. However, for a basic example, we’ll proceed without a template.
  3. Configure Pipeline Stages: Unlike CI/CD pipelines with pre-defined stages, release pipelines allow you to customize the stages. For this example, create a single stage named “Deploy to Azure”.
  4. Add Tasks to the Stage: Within the “Deploy to Azure” stage, you’ll define the deployment task. Look for the task named “Azure App Service deploy” (or similar depending on your chosen environment).
  5. Configure Deployment Settings:
    • Azure subscription: Select the Azure subscription associated with your resources.
    • App Service name: Specify the name of your Azure App Service instance (where you want to deploy the containerized application). This service can be pre-provisioned beforehand.
    • Resource group: Choose the resource group where your App Service resides (if using resource groups).
    • Image source: Select “ACR” as the image source and provide the details:
      • ACR name: Enter the name of your Azure Container Registry.
      • Image name: Specify the name of the Docker image you want to deploy (e.g., <your-acr-name><your-app-name>:<image-tag>).
      • Tag: Leave this blank to use the latest image tag from your CI/CD pipeline.
  6. Environment Variables (Optional): If your application requires specific environment variables during deployment, you can define them within the release pipeline for this stage.
  7. Review and Deploy: Once you’ve filled in all the details, carefully review your configuration. If everything looks good, click “Save” to create the release pipeline. Triggering this pipeline will initiate the deployment of your containerized Java application to the designated Azure environment.

By implementing this optional stage, you’ve automated the deployment process, ensuring a seamless transition of your application from code commit to production environment. Remember, this is a simplified example, and production deployments might involve additional considerations like multi-stage deployments, blue/green deployments, and integration with configuration management tools.

3.6 Triggering the Pipeline

The magic of CI/CD lies in its ability to automate tasks, saving you valuable time and effort. A key aspect of this automation is the seamless triggering of your build pipeline whenever changes are introduced to your codebase. This is achieved through the integration between Azure DevOps, your Git repository, and a feature called “continuous triggers.”

Here’s a breakdown of how code changes trigger the build pipeline:

  1. Configure Continuous Triggers: Within your Azure DevOps project, navigate to “Pipelines” and select the build pipeline you created earlier (e.g., the one defined in the azure-pipelines.yml file).
  2. Access Triggers Tab: Within the pipeline configuration, locate the “Triggers” tab. This tab allows you to define various conditions that will initiate the pipeline execution.
  3. Enable CI Trigger: Look for the option labeled “CI” or “Continuous Integration” trigger. This trigger specifically monitors your Git repository for changes.
  4. Branch Filters (Optional): By default, the CI trigger might be set to fire on any branch push. You can refine this behavior using branch filters. For instance, you might configure the pipeline to trigger only on pushes to the “main” branch or specific development branches.
  5. Save and Commit: Once you’ve enabled the CI trigger and configured optional branch filters, save the changes to your pipeline configuration.

Now, whenever you commit code changes and push them to your Git repository, Azure DevOps will automatically detect those changes. If the push aligns with the configured branch filters (if any), the CI trigger will fire, initiating the build pipeline. This pipeline will then execute the defined stages, fetching code from Git, building your Java application, building and pushing the Docker image to your ACR, and potentially deploying to your Azure environment (if you’ve implemented a release pipeline).

This continuous triggering ensures that your CI/CD pipeline stays in sync with your development process. Every code change triggers a build and potential deployment, allowing you to catch potential issues early and deliver updates to your application faster. Remember, you can further customize your triggers based on specific needs, such as using pull requests as triggers instead of direct pushes.

4. Conclusion

This tutorial has equipped you with the knowledge and tools to establish a robust CI/CD pipeline for your Java applications using Azure DevOps, Docker containers, and Git version control. By leveraging automated builds, deployments, and seamless triggering on code changes, you’ve streamlined your development workflow, freeing yourself to focus on crafting exceptional code.

Remember, this is just the beginning! As you delve deeper into CI/CD, explore advanced functionalities like unit testing integration, security scans within the pipeline, and multi-stage deployments for production environments. Embrace the power of automation to accelerate your development cycles, deliver high-quality Java applications faster, and gain a competitive edge.

Eleftheria Drosopoulou

Eleftheria is an Experienced Business Analyst with a robust background in the computer software industry. Proficient in Computer Software Training, Digital Marketing, HTML Scripting, and Microsoft Office, they bring a wealth of technical skills to the table. Additionally, she has a love for writing articles on various tech subjects, showcasing a talent for translating complex concepts into accessible content.
Notify of

This site uses Akismet to reduce spam. Learn how your comment data is processed.

Inline Feedbacks
View all comments
Back to top button