Spring Boot MySQL Nginx Gitlab-CI Docker on VPS

by Didin J. on Jan 01, 2021 Spring Boot MySQL Nginx Gitlab-CI Docker on VPS

A comprehensive step by step Spring Boot and MySQL guide to continuous integration using Gitlab-CI, Docker, Docker-Compose on VPS

In this Spring Boot and MySQL guide, we will implement a part of DevOps in the Spring Boot application to continuous integration using Gitlab-CI, Docker, and deployed to VPS such as Digital Ocean Droplet, AWS EC2, Microsoft Azure, Vultr, etc. 

This tutorial is divided into several steps:

The goal of this continuous integration is very simple. It just tests, builds and deploys the Spring Boot and MySQL app automatically to the VPS (Digital Ocean Droplet, AWS EC2, Microsoft Azure, Vultr, etc) after being pushed to the Gitlab Repository. So, the user can use this application via a browser that is served by the Nginx HTTP server while the development in progress.

We assume you already have Docker HUB and Gitlab accounts. So, we are not covering how to register or signup for Docker HUB and Gitlab.

You can watch the video tutorial on our YouTube channel here.

Let's get started with the main steps!


Step #1: Setup Docker HUB Repository

To set up the Docker HUB repository, go to hub.docker.com then log in using your account. 

Spring Boot MySQL Nginx Gitlab-CI Docker on VPS - img 1
Spring Boot MySQL Nginx Gitlab-CI Docker on VPS - img 2

Click a Create Repository button on the Docker Dashboard page. Fill the repository name and make it private then click a Create Button.

Spring Boot MySQL Nginx Gitlab-CI Docker on VPS - img 3

Now, the Docker HUB repository is ready to use. We will use the docker command later in the Gitlab and VPS.

Spring Boot MySQL Nginx Gitlab-CI Docker on VPS - img 4


Step #2: Prepare a VPS with Docker, Docker Compose, and Gitlab Runner

We assume that you have a fresh VPS from your provider (Digital Ocean Droplet, AWS EC2, Microsoft Azure, Vultr, etc) and are able to connect to it via SSH. Ours is using Digital Ocean Droplet, so the just need to add my SSH keys to the VPS. First, create a new SSH key using this command.

cd ~/.ssh/
ssh-keygen -f myvps-id -t rsa

Leave the passphrase blank by clicking on enter a few times and you will see this console for successful SSH key generation.

Your identification has been saved in myvps-id.
Your public key has been saved in myvps-id.pub.
The key fingerprint is:
SHA256:GVQ2ag/pUcqwCqeaXhEwJQ4aAnS/pKvmCOvFPel0pYA [email protected]
The key's randomart image is:
+---[RSA 3072]----+
|B=.o  . ..=      |
|=.= .  = * .     |
|.....o. X        |
|   +=..o *       |
|  .E.o  S..      |
| o. + o o        |
|+  = = o         |
|+o+ o o          |
|*=   .           |
+----[SHA256]-----+

In Digital Ocean, we can access the VPS through the console. Just start the Droplets console in the Digital Ocean dashboard then log in using the provided root username and password.

Spring Boot MySQL Nginx Gitlab-CI Docker on VPS - img 5

Next, edit the SSH authorized_keys file.

nano ~/.ssh/authorized_keys

Back to your terminal then show the previously created SSH public key.

cat ~/.ssh/myvps-id.pub

Copy the content of ~/.ssh/myvps-id.pub then pastes it to the authorized_keys in the Digital Ocean Droplets console. Save the authorized_keys file then close the console. Next, try to connect to the VPS using SSH from your terminal.

ssh [email protected]

Now, the VPS is ready to use for Gitlab CI/CD deployment.

Spring Boot MySQL Nginx Gitlab-CI Docker on VPS - img 6

Next, we need to install Docker, Docker Compose, and Gitlab Runner. For that, type this command to update the existing default list of Ubuntu packages

apt update

Install package to let APT transport over HTTPS.

apt install apt-transport-https ca-certificates curl software-properties-common

Add GPG Key.

curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo apt-key add -

Add Docker repository to the APT source.

add-apt-repository "deb [arch=amd64] https://download.docker.com/linux/ubuntu bionic stable"

Update again the list of packages.

apt update

Install Docker-CE from the Docker repository.

apt install docker-ce

Check the status of installed and running Docker CE.

systemctl status docker

The successful Docker installation will show the output like this.

● docker.service - Docker Application Container Engine
   Loaded: loaded (/lib/systemd/system/docker.service; enabled; vendor preset: enabled)
   Active: active (running) since Wed 2020-12-23 03:38:49 UTC; 22min ago
     Docs: https://docs.docker.com
 Main PID: 11281 (dockerd)
    Tasks: 8
   CGroup: /system.slice/docker.service
           └─11281 /usr/bin/dockerd -H fd:// --containerd=/run/containerd/containerd.sock

Dec 23 03:38:49 ubuntu-s-1vcpu-1gb-sgp1-01 dockerd[11281]: time="2020-12-23T03:38:49.030947689Z" level=info msg="ClientConn switching balancer to \"pick_first\"" module=grpc
Dec 23 03:38:49 ubuntu-s-1vcpu-1gb-sgp1-01 dockerd[11281]: time="2020-12-23T03:38:49.089856532Z" level=warning msg="Your kernel does not support swap memory limit"
Dec 23 03:38:49 ubuntu-s-1vcpu-1gb-sgp1-01 dockerd[11281]: time="2020-12-23T03:38:49.090228665Z" level=warning msg="Your kernel does not support CPU realtime scheduler"
Dec 23 03:38:49 ubuntu-s-1vcpu-1gb-sgp1-01 dockerd[11281]: time="2020-12-23T03:38:49.090615892Z" level=info msg="Loading containers: start."
Dec 23 03:38:49 ubuntu-s-1vcpu-1gb-sgp1-01 dockerd[11281]: time="2020-12-23T03:38:49.237818403Z" level=info msg="Default bridge (docker0) is assigned with an IP address 172.17.0.0
Dec 23 03:38:49 ubuntu-s-1vcpu-1gb-sgp1-01 dockerd[11281]: time="2020-12-23T03:38:49.321685622Z" level=info msg="Loading containers: done."
Dec 23 03:38:49 ubuntu-s-1vcpu-1gb-sgp1-01 dockerd[11281]: time="2020-12-23T03:38:49.352831979Z" level=info msg="Docker daemon" commit=f001486 graphdriver(s)=overlay2 version=20.1
Dec 23 03:38:49 ubuntu-s-1vcpu-1gb-sgp1-01 dockerd[11281]: time="2020-12-23T03:38:49.353406920Z" level=info msg="Daemon has completed initialization"
Dec 23 03:38:49 ubuntu-s-1vcpu-1gb-sgp1-01 systemd[1]: Started Docker Application Container Engine.
Dec 23 03:38:49 ubuntu-s-1vcpu-1gb-sgp1-01 dockerd[11281]: time="2020-12-23T03:38:49.399081326Z" level=info msg="API listen on /var/run/docker.sock"

Next, try to log in to Docker using your Docker username and password.

docker login -u didinj

If you get this error.

Error saving credentials: error storing credentials - err: exit status 1, out: `Cannot autolaunch D-Bus without X11 $DISPLAY`

The solution for that error is storing Docker credentials using docker-credentials-pass. First, download the docker-credentials-pass.

wget https://github.com/docker/docker-credential-helpers/releases/download/v0.6.3/docker-credential-pass-v0.6.3-amd64.tar.gz

Extract that downloaded Tar file.

tar -xvf docker-credential-pass-v0.6.3-amd64.tar.gz

Change the permission for this file.

chmod u+x docker-credential-pass

Move that file to /usr/bin.

mv docker-credential-pass /usr/bin

Run the GPG command to generate the GPG key.

gpg --generate-key

Fill out all of the questions then enter your passphrase. If the key generation hangs or freezes in this part.

We need to generate a lot of random bytes. It is a good idea to perform
some other action (type on the keyboard, move the mouse, utilize the
disks) during the prime generation; this gives the random number
generator a better chance to gain enough entropy.

SSH again in another terminal tab or shell then run this command.

dd if=/dev/vda1 of=/dev/zero

Change "/dev/vda1" to your VPS main volume then wait for a few minutes. You will see this in the previous terminal.

gpg: /root/.gnupg/trustdb.gpg: trustdb created
gpg: key F49BA7D33A1D4B78 marked as ultimately trusted
gpg: directory '/root/.gnupg/openpgp-revocs.d' created
gpg: revocation certificate stored as '/root/.gnupg/openpgp-revocs.d/40901CB6F51D9069FB4CDC8DF49BA7D33A1D4B78.rev'
public and secret key created and signed.

pub   rsa3072 2020-12-23 [SC] [expires: 2022-12-23]
      40901CB6F51D9069FB4CDC8DF49BA7D33A1D4B78
uid                      Didin Jamaludin <[email protected]>
sub   rsa3072 2020-12-23 [E] [expires: 2022-12-23]

Copy the characters under pub the run-pass init command with those characters (install pass package if it does not exist).

pass init 40901CB6F51D9069FB4CDC8DF49BA7D33A1D4B78

Run again this command then set the password "pass is initialized".

pass insert docker-credential-helpers/docker-pass-initialized-check

To check it, type this command then enter the previously entered passphrase.

pass show docker-credential-helpers/docker-pass-initialized-check

Now, it should show this line.

pass is initialized

To see the Docker credentials, type this command.

docker-credential-pass list

You will see the blank object.

{}

Next, create a new folder with the JSON file.

mkdir ~/.docker
nano ~/.docker/config.json

Fill that file with this.

{
"credsStore": "pass"
}

Now, the Docker login should work.

docker login -u didinj
Password: 
Login Succeeded

If you get the error "pass store is uninitialized" in a future run, run the below command.

pass show docker-credential-helpers/docker-pass-initialized-check

Next, to install the Docker Compose, simply run this command.

apt install docker-compose

Now, Docker and docker-compose are ready to work with Gitlab CI.


Step #3: Create a New Spring Boot and MySQL App

As usual, we will create a new Spring Boot and MySQL app by generating the project using start.spring.io.

Spring Boot MySQL Nginx Gitlab-CI Docker on VPS - img 7

Fill in the required fields and radio button and select the required library as described in the picture then click the Generate button. The initial Spring Boot project was downloaded. Extract then move to your Projects folder. Open this Spring Boot project with your IDE or Text Editor. Some IDE will automatically resolve and download dependencies when the project opens.

Before configuring the MySQL connection, first, we need to create a new database and user for this tutorial. We assume you have installed the MySQL or MariaDB server and client. Run this command from the terminal to enter the MySQL console.

mysql -u root

Next, create a database and its user using these MySQL commands.

create database MySpringBootApp;
create user 'MyUser'@'%' identified by 'MyPassword';
grant all on MySpringBootApp.* to 'MyUser'@'%';
flush privileges;

Next, configure the MySQL connection by opening and editing "src/main/resources/application.properties" and then add these lines of MySQL connection parameter.

spring.jpa.database-platform=org.hibernate.dialect.MySQL5Dialect
spring.jpa.hibernate.ddl-auto=update
spring.datasource.url=jdbc:mysql://${MYSQL_HOST:localhost}:3306/MySpringBootApp?useUnicode=true&useJDBCCompliantTimezoneShift=true&useLegacyDatetimeCode=false&serverTimezone=Asia/Jakarta
spring.datasource.username=MyUser
spring.datasource.password=MyPassword

Also, we need to configure the embedded database for the test because there's no MySQL server in the Gitlab CI jobs. For that, create a folder and a property file in the test folder.

mkdir src/test/resources
touch src/test/resources/application.properties

Fill that file with this H2 Database connection parameters.

spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:mem:db;DB_CLOSE_DELAY=-1
spring.datasource.username=sa
spring.datasource.password=sa

Also, we need to install the embedded package for the H2 database. Open and edit pom.xml then add this dependency.

<dependencies>
...
    <dependency>
        <groupId>com.h2database</groupId>
        <artifactId>h2</artifactId>
    </dependency>
</dependencies>     

Next, create a package and a simple model/domain/entity class "src/main/java/com/djamware/myspringbootapp/entity/MyProduct.java" then replace all Java code with this.

package com.djamware.myspringbootapp.entity;

import lombok.*;
import org.springframework.data.annotation.Id;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import java.io.Serializable;

@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Entity
public class MyProduct implements Serializable {
    @javax.persistence.Id
    @Id
    @GeneratedValue(strategy= GenerationType.AUTO)
    private Integer id;

    private String prodName;

    private String prodDesc;

    private Double prodPrice;
}

Next, create a package and a simple repository class "src/main/java/com/djamware/myspringbootapp/repository/MyProductRepository.java" then replace all Java code with this.

package com.djamware.myspringbootapp.repository;

import com.djamware.myspringbootapp.entity.MyProduct;
import org.springframework.data.repository.CrudRepository;

public interface MyProductRepository extends CrudRepository<MyProduct, Integer> {
}

Next, create a package and a simple REST controller class "src/main/java/com/djamware/myspringbootapp/repository/MyProductController.java" then replace all Java code with this.

package com.djamware.myspringbootapp.controller;

import com.djamware.myspringbootapp.entity.MyProduct;
import com.djamware.myspringbootapp.repository.MyProductRepository;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;

@Controller
@RequestMapping(path="/product")
public class MyProductController {

    private final MyProductRepository myProductRepository;

    public MyProductController(MyProductRepository myProductRepository) {
        this.myProductRepository = myProductRepository;
    }

    @GetMapping()
    public @ResponseBody
    Iterable<MyProduct> getAllUsers() {
        return myProductRepository.findAll();
    }

    @PostMapping()
    public @ResponseBody String addNewUser (@RequestBody MyProduct myProduct) {
        MyProduct newProduct = MyProduct.builder()
                .prodName(myProduct.getProdName())
                .prodDesc(myProduct.getProdDesc())
                .prodPrice(myProduct.getProdPrice())
                .build();
        myProductRepository.save(newProduct);
        return "Saved";
    }
}

Next, run the application by clicking the play button in the IDE or type this command in the terminal.

mvn spring-boot:run

If there's no error found, that means the Spring Boot MySQL app is ready to deploy.


Step #4: Dockerize Spring Boot and MySQL App

While your IDE still opens the Spring Boot MySQL project. Add this file to the root of the project folder.

touch Dockerfile

Add these lines of Docker commands.

FROM openjdk:11
LABEL maintainer="[email protected]"
VOLUME /tmp
EXPOSE 8080
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} myspringbootapp-0.0.1-SNAPSHOT.jar
ENTRYPOINT ["java","-Dspring.profiles.active=dev","-jar","myspringbootapp-0.0.1-SNAPSHOT.jar"]

We are using Docker OpenJDK-11 image same JDK version as the previously generated Spring Boot app. The volume to save the Docker image is set to /tmp directory of the Host. A port using standard Java Web app port 8080. ARG JAR_FILE means the built jar file save to the target directory with the JAR extension. COPY and ENTRYPOINT should point to the Spring Boot jar file name exactly as the artifactId and version in the pom.xml.

Next, to simplify and Dockerize every server to run the Spring Boot application on VPS, we will use Docker Compose. For that, SSH again to the VPS then create a docker-compose.yml file in the home or default ROOT directory.

nano docker-compose.yml

Then fill that file with these Docker Compose parameters.

version: '3.3'
services:
  nginx:
    container_name: nginx
    image: nginx:1.13
    restart: always
    ports:
      - 80:80
      - 443:443
    network_mode: host
    hostname: localhost
    volumes:
      - ./nginx/conf.d:/etc/nginx/conf.d
    depends_on:
      - springboot
    environment:
      TZ: Asia/Jakarta
    healthcheck:
      test: "exit 0"
  springboot:
    restart: always
    image: didinj/myspringbootapp
    container_name: springboot
    ports:
      - 8080:8080
    network_mode: host
    hostname: localhost
    healthcheck:
      test: "exit 0"

We use Nginx as an HTTP reverse proxy for the Spring Boot app. The Nginx container use image from the Docker repository. So, the configuration will use a separate file. For that, create a new folder and Nginx configuration file.

mkdir nginx
mkdir conf.d
nano nginx/conf.d/app.conf

Then fill that configuration file with these lines of Nginx configuration.

server {
    listen 80;
    location / {
        proxy_pass http://127.0.0.1:8080/;
    }
}

As you see in the docker-compose file, that configuration file will copy to the Nginx container to this directory /etc/nginx/conf.d. The Spring boot container uses a Docker image from the docker hub repository. For MySQL, we will run it using the Docker run command because we have an error using Docker Compose for MySQL.

[ERROR] [Entrypoint]: Database is uninitialized and password option is not specified
    You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD

So, we will create an environment variables file to load the MySQL root password, database, user, and password. Type this command to create it.

nano mysql.env

Add these lines of MYSQL parameters.

MYSQL_ROOT_PASSWORD=my-secret-pw
MYSQL_DATABASE=MySpringBootApp
MYSQL_USER=MyUser
MYSQL_PASSWORD=MyPassword

To store the MySQL data in the host volume, create a new directory.

mkdir mysql-data

Next, run the Docker for MySQL by typing this command.

docker run --name mysql --network host --env-file=mysql.env --volume=mysql-data:/var/lib/mysql -d mysql:latest

To check the running MySQL Docker container, type this command.

docker ps -a


Step #5: Setup Gitlab and Gitlab-CI

To set up a Gitlab repository and Gitlab CI/CD, login to Gitlab.com using your own account or your organization account. Click the New Project button then click create a blank project.

Spring Boot MySQL Nginx Gitlab-CI Docker on VPS - img 8

Fill the project slug name with the Spring boot docker image name as in the Docker compose file. Leave other options as default then click the Create Project button. Next, back to the terminal and Spring boot project folder. Type these commands to add the Gitlab repository.

git init
git remote add origin [email protected]:didinj/myspringbootapp.git
git add .
git commit -m "Initial commit"
git push -u origin master

Next, back to the Gitlab project dashboard and click the settings -> CI/CD menu on the left navigation.

Spring Boot MySQL Nginx Gitlab-CI Docker on VPS - img 9

Expand Variables then add Variable. Enter all required variables that will use in the Gitlab-CI.yml file (SSH_PRIVATE_KEY, DOCKER_USER, DOCKER_PASSWORD, DOCKER_REPO, APP_NAME, SERVER).

Spring Boot MySQL Nginx Gitlab-CI Docker on VPS - img 10

Which is SSH_PRIVATE_KEY value is the SSH key that was previously generated before SSH to the VPS. DOCKER_USER and DOCKER_PASSWORD are the Docker HUB username and password. DOCKER_REPO is the Docker HUB repository that was previously created. APP_NAME is the Spring boot app name. SERVER is the VPS IP address.

Next, we need to install the Gitlab Runner in the VPS. SSH again to the VPS then type this command to add the official Gitlab repository.

curl -L "https://packages.gitlab.com/install/repositories/runner/gitlab-runner/script.deb.sh" | sudo bash

Install the latest version of Gitlab runner.

export GITLAB_RUNNER_DISABLE_SKEL=true; sudo -E apt-get install gitlab-runner

Next, back to the Gitlab Settings CI/CD and expand the Runners.

Spring Boot MySQL Nginx Gitlab-CI Docker on VPS - img 11

Copy the Registration token in the Specific Runners then back to the VPS and type this command to set up the runners.

gitlab-runner register

Enter the Gitlab instance URL.

https://gitlab.com/

Enter the registration token by pasting the previously copied token then pressing enter. Enter a description for the runner.

myspringbootapp-runner

Enter tags for the runner.

test,myspringbootapp,master,dev

Enter an executor: virtualbox, docker-ssh+machine, kubernetes, parallels, shell, ssh, docker+machine, custom, docker, docker-ssh.

docker

Enter the default Docker image.

openjdk:11

If you refresh the Gitlab Settings CI/CD Runners, you will see the runners activated for this project.

Spring Boot MySQL Nginx Gitlab-CI Docker on VPS - img 12

Next, back to the IDE or Text editor then create a new file in the root of the project folder.

touch .gitlab-ci.yml

Fill the .gitlab-ci.yml with this.

stages:
  - test
  - build jar
  - build and push docker image
  - deploy
test_job:
  image: maven:3.6.2-jdk-11-slim
  stage: test
  script:
    - pwd
    - mvn clean
    - mvn compile
    - mvn test
  tags:
    - test
  only:
    - master
build:
  image: maven:3.6.2-jdk-11-slim
  stage: build jar
  script:
    - mvn clean install -Dspring.profiles.active=dev && mvn package -B -e -Dspring.profiles.active=dev
  artifacts:
    paths:
      - target/*.jar
  only:
    - master
docker build:
  services:
    - docker:19.03.13-dind
  image: docker:19.03.13-dind
  stage: build and push docker image
  script:
    - docker build --build-arg SPRING_ACTIVE_PROFILE=dev -t $DOCKER_REPO:latest .
    - docker login -u $DOCKER_USER -p $DOCKER_PASSWORD docker.io
    - docker push $DOCKER_REPO:latest
  only:
    - master
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
  script:
    - pwd
    - ssh root@$SERVER "docker login -u $DOCKER_USER -p $DOCKER_PASSWORD docker.io; docker stop $APP_NAME; docker system prune -a -f; docker run --name mysql --network host --env-file=mysql.env --volume=mysql-data:/var/lib/mysql -d mysql:latest; docker pull $DOCKER_REPO:latest; docker-compose up -d; docker logout"
  only:
    - master

We are creating 4 stages of Gitlab CI/CD. First test the Spring Boot app, build the Spring Boot app to JAR, build the Docker images then push to the Docker repository, and SSH to VPS then pull the Docker image from the Docker repository then run the Docker compose.

Next, commit and push the updated Spring Boot app with .gitlab-ci.yml file.

git add .
git commit -m "Add .gitlab-ci.yml"
git push

If the Gitlab CI jobs working, you should see this at the checkout for all stages in the Gitlab CI/CD pipelines.

Spring Boot MySQL Nginx Gitlab-CI Docker on VPS - img 13

To check the deployed Spring Boot application, back to the VPS by SSH then type this command.

docker ps -a

You should see this Docker container running up and healthy.

Spring Boot MySQL Nginx Gitlab-CI Docker on VPS - img 14

Finally, you can check the application by accessing it through the VPS IP address. In the terminal type this CURL POST command after exiting the VPS SSH.

curl -i -X POST -H "Content-Type: application/json" -d '{"prodName":"Dummy Product 1","prodDesc":"The Fresh Dummy Product in The world part 1","prodPrice":100}' http://139.59.243.46/product

The successful response should be like this.

HTTP/1.1 200 
Server: nginx/1.13.12
Date: Thu, 31 Dec 2020 23:02:17 GMT
Content-Type: text/plain;charset=UTF-8
Content-Length: 5
Connection: keep-alive

And GET request to the API.

curl http://139.59.243.46/product

The successful response should be like this.

[{"id":1,"prodName":"Dummy Product 1","prodDesc":"The Fresh Dummy Product in The world part 1","prodPrice":100.0},{"id":2,"prodName":"Dummy Product 1","prodDesc":"The Fresh Dummy Product in The world part 1","prodPrice":100.0}]

That it's, The Spring Boot MySQL Nginx Gitlab-CI Docker on VPS tutorial. You can get the full source code from our GitHub.

That is just the basics. If you need more deep learning about Java and Spring Framework you can take the following cheap course:

Thanks!