The multi-stage builds feature of Docker allows you to specify multiple FROM statements in your Dockerfile. Each FROM statement can use a different base image, and each FROM statement begins a new build stage. Multi-stage builds for Java applications bring benefits such as high security, fast building, and small image size. This topic uses a Java application that is developed with Maven and uses GitHub to manage its source code repository as an example to describe how to build an image for a Java application by using Alibaba Cloud Container Registry and multi-stage builds.

Prerequisites

Background information

Common issues in Docker image building

The image building service of Container Registry uses a Dockerfile to build the final image of an application. During this process, you may encounter the following issues:
  • It is difficult to write a Dockerfile.

    When you get used to using the powerful frameworks of programming languages, especially Java, to conveniently build applications, you may find it difficult to write a Dockerfile to build an application image.

  • The final image may be large in size.

    When you build an image, you may include the application compilation, test, and packaging processes in the same Dockerfile. Each command in the Dockerfile creates a layer of the image, which complicates the structure of the image and enlarges the image size.

  • The source code may be leaked.

    You may package the source code of your application in the final image, which may lead to code leakage.

Benefits of multi-stage builds

When you use multi-stage builds in a Dockerfile to build images for applications developed with compilation languages such as Java, you can enjoy the following benefits:
  • The final image is built in a secure way.

    In the first stage of image building, you must specify an appropriate base image, copy source code to the base image, download application dependencies, compile the source code, test the application, and package the application. In the second stage, you must specify another appropriate base image and copy runtime dependency files generated in the first stage to the base image. In this way, the final image does not contain the source code.

  • The final image has fewer layers and a smaller size.

    The final image only contains a base image and compiled artifacts, which occupy few layers and require small storage size.

  • The final image is built at a fast speed.

    You can use build tools such as Docker or Buildkit to concurrently run multiple build processes, which improves the build speed.

Step 1: Create a Dockerfile with multi-stage builds

This topic uses a Java application that is developed with Maven and uses GitHub to manage its source code repository as an example. Create a Dockerfile in the Java project and add the following configuration to the Dockerfile:
Note As shown in the following configuration, the Dockerfile includes two build stages.
  • First stage: Specify the Maven base image, copy source code to the base image, and then run the RUN command to create a JAR package. If the Java application is developed with Gradle, you can specify a Gradle base image in this stage.
  • Second stage: Copy the JAR package generated in the first stage to the OpenJDK image and run the CMD command to generate the final image.
# First stage: complete build environment
FROM maven:3.5.0-jdk-8-alpine AS builder

# add pom.xml and source code
ADD ./pom.xml pom.xml
ADD ./src src/

# package jar
RUN mvn clean package

# Second stage: minimal runtime environment
From openjdk:8-jre-alpine

# copy jar from the first stage
COPY --from=builder target/my-app-1.0-SNAPSHOT.jar my-app-1.0-SNAPSHOT.jar

EXPOSE 8080

CMD ["java", "-jar", "my-app-1.0-SNAPSHOT.jar"]

Step 2: Authorize Container Registry to access the source code repository

Log on to the Container Registry console and authorize Container Registry to access the source code repository. This section uses GitHub as an example of the code repository hosting service.

  1. Select a region from the drop-down list at the top of the page. In the left-side navigation pane, choose Default Instance > Code Source.
  2. Click Bind Account for the GitHub service. In the GitHub dialog box, click Go to the source code repository to bind account. On the page that appears, enter your username and password to log on to GitHub.
  3. On the authorization page, click Authorize AliyunDeveloper.
    1
    Go to the Code Source page. Check whether the status of the GitHub service appears as Bound.

Step 3: Create an image repository

  1. In the left-side navigation pane, choose Default Instance > Repositories. On the Repositories page, click Create Repository.
  2. Set the parameter as required.
    2
    Parameter Description
    Region The region where the image repository resides. Container Registry supports 23 regions around the world.
    Namespaces The namespace to which the image repository belongs. An image repository belongs to only one namespace, whereas a namespace can contain multiple image repositories.
    Repository Name The name of the image repository.
    Repository Type The type of the image repository. Valid values: Public and Private. You can only push images to an image repository after you log on to the image repository, regardless of the repository type.
    • Public: You can pull images from the image repository without logging on to the image repository.
    • Private: You can only pull images from the image repository after logging on to the image repository on the Docker client.
    Summary The brief description of the image repository.
    Description The detailed description of the image repository.
  3. Click Next. In the Code Source step, specify the code source and build settings.
    • Code Source: the service or platform where the source code repository is hosted. Click the GitHub tab and select the source code repository that you have authorized Container Registry to access in Step 2: Authorize Container Registry to access the source code repository.
    • Build Settings: the mode in which images are built.
      1. Automatically Build Images When Code Changes: If you select this option, an image is automatically built when code is committed from a branch.
      2. Build With Servers Deployed Outside Mainland China: If you select this option, images are built in a data center outside mainland China and then pushed to the image repository.
      3. Build Without Cache: If you select this option, the system pulls the dependent base image for every image to be built. This may slow down the build time.
  4. Click Create Repository.
    On the Repositories page, check whether the image repository that you have created appears.

Step 4: Build an image

  1. In the left-side navigation pane, choose Default Instance > Repositories. On the Repositories page, find the target image repository and click Manage in the Actions column.
  2. In the left-side navigation pane, click Build. In the Build Rules section of the page that appears, click Add Build Rule.
  3. In the Add Build Rule dialog box, set the parameters as required.
    3
    Parameter Description
    Type The type of content pushed to the source code repository that will trigger the build rule. Valid values: Branch and Tag.
    Branch/Tag The code branch or tag that will trigger the build rule.
    Dockerfile Directory The directory where the Dockerfile is stored. The specified directory is a relative directory, with the root directory of the code branch as its parent directory.
    Dockerfile Filename The name of the Dockerfile. The default file name is Dockerfile.
    Tags The tag of the image to be built.
  4. Click Ok.
  5. In the Build Rules section, find the created rule and click Build in the Actions column.
    After you start the build, a build record is generated in the Build Log section. When the status of the build record becomes Successful, the image is built.

Result

  • Check whether the image is built

    In the left-side navigation pane, choose Default Instance > Repositories. On the Repositories page, find the target image repository and click Manage in the Actions column. In the left-side navigation pane, click Tags. On the Tags page, find the image that has been built.

  • Check the number of layers in the image

    Log on to the image repository on the Docker client. Run the relevant commands to pull the image and query the number of layers in the image. As shown in the returned result, only the JAR package compiled in the first stage is added to the image, which occupies small storage space, and the image contains four layers.

    docker pull registry.cn-hangzhou.aliyuncs.com/docker-builder/java-multi-stage:master
    
    docker inspect registry.cn-hangzhou.aliyuncs.com/docker-builder/java-multi-stage:master | jq -s .[0] [0].RootFS
    {
      "Type": "layers",
      "Layers": [
        "sha256:f1b5933fe4b5f49bbe8258745cf396afe07e625bdab3168e364daf7c956b6b81",
        "sha256:9b9b7f3d56a01e3d9076874990c62e7a516cc4032f784f421574d06b18ef9aa4",
        "sha256:edd61588d12669e2d71a0de2aab96add3304bf565730e1e6144ec3c3fac339e4",
        "sha256:2be89a7ccd49878fb5f09d6dfa5811ce09fc1972f0a987bbb5a006992aa3dff3"
      ]
    }
  • Run the image and check the result

    Run the following command on the Docker client to run the image and check the result:

    docker run -ti registry.cn-hangzhou.aliyuncs.com/docker-builder/java-multi-stage:master
    Hello World!