Docker and Spring Boot is a very popular combination, we will take advantage of GitLab CI and automatically build, push and run Docker images on the application server.

GitLab CI

Gitlab CI/CD services are part of Gitlab. When developers push code to the GitLab repository, the GitLab CI automatically builds, tests, and stores the latest code changes in a user-specified environment.

Some of the main reasons for choosing GitLab CI:

  1. Easy to learn, easy to use and extensible
  2. Easy to maintain
  3. Integration is easy
  4. CI is completely part of the GitLab repository
  5. Good Docker integration
  6. Container Registry – basically your own private Docker Hub
  7. GitLab CI is a good solution in terms of cost. You get 2,000 minutes of free build time per month, which is more than enough for some projects

Why did GitLab CI surpass Jenkins

This is undoubtedly a widely discussed topic, but we won’t delve into it in this article. Both GitLab CI and Jenkins have strengths and weaknesses, and they are very powerful tools.

So why GitLab?

As mentioned earlier, Gitlab CI is part of the Gitlab repository, which means that when we have Gitlab, there is no need to install Gitlab CI and no additional maintenance. And all you need to do is write a.gitlab-ci.yml file (more on that later), and you’re done with CI.

With Jenkins for small projects, you’ll have to configure everything yourself. Often, you’ll also need a dedicated Jenkins server, which also requires additional costs and maintenance.

Prerequisite to use GitLab CI

If you need any help with these prerequisites, I have provided links to the corresponding guidelines.

  1. You have pushed the Spring Boot project on GitLab
  2. You have Docker installed on the application server
  3. You have image hosting of Docker images (Docker Hub will be used in this article)
  4. You have generated SSH RSA keys on the server (Guide)

What are you going to create

You will create Dockerfile and.gitlab-ci.yml, which will automatically be used for:

  1. Build the application Jar file
  2. Build the Docker image
  3. Push the image to the Docker repository
  4. Run the image on the application server

Basic Project Information

The Spring Boot application for this article was generated through Spring Initializr. This is a Maven project built on Java 8 or Java11. Later, we’ll look at how Java 8 and Java 11 affect Docker images.

Docker file

Let’s start with Dockerfile.

MAVEN_BUILD#FROM Maven: 3.5.2-JDK-8-alpine MAVEN_BUILD FOR JAVA 8ARG SPRING_ACTIVE_PROFILEMAINTAINER JasminCOPY pom.xml /build/COPY src /build/src/WORKDIR /build/RUN mvn clean install -Dspring.profiles.active=$SPRING_ACTIVE_PROFILE && mvn package -B -e -Dspring.profiles.active=$SPRING_ACTIVE_PROFILEFROM  openjdk:11-slim#FROM openjdk:8-alpine FOR JAVA 8WORKDIR /appCOPY --from=MAVEN_BUILD /build/target/appdemo-*.jar /app/appdemo.jarENTRYPOINT ["java", "-jar", "appdemo.jar"]Copy the code

Java version

Let’s look at the differences between Java 8 and 11 from a Docker perspective. Long story short: This is the size and deployment time of the Docker image.

Docker images built on Java 8 will be significantly smaller than Java 11-based images. This also means faster build and deployment times for Java 8 projects.

Java 8- Build time: about 4 minutes, image size is about 180 MB

Java 11- Build time: about 14 minutes, image size is about 480 MB

Note: In practice, these numbers may vary.

Docker mirror

As you’ve seen in the previous example, we have huge differences in application image size and build time due to the Java version. The actual reason behind this is that different Docker images are used in dockerfiles.

If we take a look at the Dockerfile again, the real reason the Java 11 image is so big is because it includes the Alpine version of the unverified/tested Open-JDK :11 image.

If you are not familiar with the OpenJDK image version, you are advised to read the official OpenJDK Docker documentation. Here, you can find a description of the images for each OpenJDK version.

Note: Dynamic variables

In ENTRYPOINT, environment-related attributes can only be written as follows:

ENTRYPOINT [ “ java”,“ -Dspring.profiles.active = development”,“ -jar”,“ appdemo.jar” ]
Copy the code

To make it dynamic, you want to simply convert it to:

ENTRYPOINT [" Java ", "-dspring.profiles. Active = $SPRINT_ACTIVE_PROFILE", "-jar", "appDemo.jar"]Copy the code

Previously, this was not possible, but fortunately this will be fixed in.gitlab-ci.yml via ARG SPRING_ACTIVE_PROFILE.

gitlab-ci.yml

There is very little to prepare before writing this file. Basically, what we want is for the code to be automatically deployed on the appropriate environment as soon as it is pushed.

Create. Env files and branches

We first need to create the branches and.env files that contain the environment. Each branch actually represents the environment in which our application will run.

We will deploy our application in three different environments: Development, test, and production. This means we need to create three branches. Our Dev, QA, and Prod applications will run on different servers and will have different Docker container labels, ports, and SSH keys. This requires that our gitlab-ci.yml file be dynamic. We can solve this problem by creating separate. Env files for each environment.

.develop.env

.qa.env

.master.env

Important: When naming these files, there is a simple rule: use the GitLab branch name, so the file name should look like this:. $ BRANCH_NAME.env

For example, here is the.develop.env file.

export SPRING_ACTIVE_PROFILE='development'export DOCKER_REPO='username/demo_app:dev'export APP_NAME='demo_app_dev'export PORT = '8080' export SERVER_IP = '000.11.222.33' export SERVER_SSH_KEY = "$DEV_SSH_PRIVATE_KEY"Copy the code

Important notes related to.env files:

  • SPRING_ACTIVE_PROFILE: It is self-explanatory which Spring application properties we want to use. DOCKER_REPO: This is the repository of Docker images; The only thing we need to notice here is the Docker Image TAG. For each environment we will use different tags, which means we will use dev, QA and PROd tags.

Our Docker center looks something like this.

 

As you can see, there is a repository with three different tags, and each tag (application version) is updated every time code is pushed to the GitLab branch.

  • APP_NAME: This property is very important, it is the name of the container. If you do not set this property, Docker will randomly name your container. This can be a problem because you won’t be able to stop running the container in a clean way.
  • Port: This is the port on which we want the Docker container to run.
  • SERVER_IP: IP address of the server used by the application. Typically, each environment will be on a different server.
  • SERVER_SSH_KEY: This is the SSH key we have generated on each server. $DEV_SSH_PRIVATE_KEY is actually a variable from the GitLab repository.

Create the GitLab variable

The last thing you need to do is create the GitLab variable.

Open your GitLab repository and go to: Settings -> CI/CD. In the Variables section, add a new variable:

  • DOCKER_USER: user name used to access Docker Hub or other image hosting
  • DOCKER_PASSWORD: password used to access image hosting
  • $ENV_SSH_PRIVATE_KEY: SSH private key previously generated on the server.

SSH Keys:

  • You need to copy the complete KEY values, including: — BEGIN RSA PRIVATE KEY — and — END RSA PRIVATE KEY —

Finally, your GitLab variable should look like this.

 

Create the gitlab-ci.yml file

Finally, let’s create a file that puts everything together.

Services: - docker:19.03.7-dindstages: - build jar - build and push docker image-deploybuild: image: Maven: 3.6.3-JK-11-Slim Stage: build jar before_script: -source.${CI_COMMIT_REF_NAME}. Env script: - mvn clean install -Dspring.profiles.active=$SPRING_ACTIVE_PROFILE && mvn package -B -e -Dspring.profiles.active=$SPRING_ACTIVE_PROFILE artifacts: paths: - target/*.jardocker build: image: docker:stable stage: build and push docker image before_script: - source .${CI_COMMIT_REF_NAME}.env script: - docker build --build-arg SPRING_ACTIVE_PROFILE=$SPRING_ACTIVE_PROFILE -t $DOCKER_REPO . - docker login -u $DOCKER_USER  -p $DOCKER_PASSWORD docker.io - docker push $DOCKER_REPOdeploy: image: ubuntu:latest stage: deploy before_script: - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )' - eval $(ssh-agent -s) - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add - - mkdir -p ~/.ssh - chmod 700 ~/.ssh - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config - source .${CI_COMMIT_REF_NAME}.env script: - ssh root@$SERVER "docker login -u $DOCKER_USER -p $DOCKER_PASSWORD docker.io; docker stop $APP_NAME; docker system prune -a -f; docker pull $DOCKER_REPO; docker container run -d --name $APP_NAME -p $PORT:8080 -e SPRING_PROFILES_ACTIVE=$SPRING_ACTIVE_PROFILE $DOCKER_REPO; docker logout"Copy the code

Let’s explain what happened here:

Services: - docker: 19.03.7 - dindCopy the code

This is a service that allows us to use Docker in Docker. Running a Docker inside a Docker is usually not a good idea, but for this use case it is perfectly fine, because we will build the image and push it to the repository.

stages:  - build jar  - build and push docker image  - deploy
Copy the code

For each Gitlab-ci.yML file, the execution steps must first be defined. Scripts are executed in the order defined by the steps.

At each step, we must add the following sections:

before_script: - source .${CI_COMMIT_REF_NAME}.env
Copy the code

This is just preloading the env.files you created earlier. Automatically inject variables based on which branch is running. (This is why we must use the branch name for the. Env file.)

These are the execution steps in our deployment process.

 

As you can see, there are three circles with green check marks to indicate that all steps have been successfully executed.

Build: image: maven: 3.6.3-JDK-11-slim stage: build jar before_script: -source.${CI_COMMIT_REF_NAME}. Env script: - mvn clean install -Dspring.profiles.active=$SPRING_ACTIVE_PROFILE && mvn package -B -e -Dspring.profiles.active=$SPRING_ACTIVE_PROFILE artifacts: paths: - target/*.jarCopy the code

This is part of the code that performs step 1, building a JAR file that you can download. This is actually an optional step, just to demonstrate how easy it is to build the JAR and download it from GitLab.

The second step is to build and push the Docker image in the Docker repository.

docker build: image: docker:stable stage: build and push docker image before_script: - source .${CI_COMMIT_REF_NAME}.env script: - docker build --build-arg SPRING_ACTIVE_PROFILE=$SPRING_ACTIVE_PROFILE -t $DOCKER_REPO . - docker login -u $DOCKER_USER  -p $DOCKER_PASSWORD docker.io - docker push $DOCKER_REPOCopy the code

For this step, we had to use the Docker: 19.03.7-Dind service. As you can see, we are using the latest stable version of Docker, we are just building the image for the appropriate environment, then authenticating the Dockerhub and pushing the image.

The final part of our script is:

deploy:  image: ubuntu:latest  stage: deploy  before_script:    - 'which ssh-agent || ( apt-get update -y && apt-get install openssh-client -y )'    - eval $(ssh-agent -s)    - echo "$SSH_PRIVATE_KEY" | tr -d '\r' | ssh-add -    - mkdir -p ~/.ssh    - chmod 700 ~/.ssh    - echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config    - source .${CI_COMMIT_REF_NAME}.env  script:    - ssh root@$SERVER "docker stop $APP_NAME; docker system prune -a -f; docker pull $DOCKER_REPO; docker container run -d --name $APP_NAME -p $PORT:8080 -e SPRING_PROFILES_ACTIVE=$SPRING_ACTIVE_PROFILE $DOCKER_REPO"
Copy the code

In this step, we use the Ubuntu Docker image, so we can SSH into our application server and run some Docker commands. Some of the code before_script is mostly from official documentation, but, of course, we can tweak it a little to suit our needs. To not validate the private key, add the following line:

- echo -e "Host *\n\tStrictHostKeyChecking no\n\n" > ~/.ssh/config
Copy the code

You can also verify the private key by referring to the guide. As you can see in the script section of the final stage, we are executing some Docker commands.

  1. Docker stop $APP_NAME Docker stop $APP_NAME (This is why we define APP_NAME in the.env file)
  2. Delete all unused Docker images: Docker system prune -a -f, this is actually not mandatory, but I want to delete all unused images on the server.
  3. Pull the latest version of the Docker image that was built and pushed in the previous stage.
  4. Finally, run the Docker image with the following command: docker container run -d –name $APP_NAME -p $PORT:8080 -e SPRING_PROFILES_ACTIVE=$SPRING_ACTIVE_PROFILE $DOCKER_REPO

Suo.im /5ZMDjf