Skip to content

Instantly share code, notes, and snippets.

Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Ikhiloya/d1d0d034c63199eed2c67a9a1a400152 to your computer and use it in GitHub Desktop.
Save Ikhiloya/d1d0d034c63199eed2c67a9a1a400152 to your computer and use it in GitHub Desktop.
This article provides developers with instructions on how to deploy a Java Spring Boot application to the DigitalOcean App Platform using Docker

How to Deploy a Spring Boot App with Docker to DigitalOcean App Platform

Digital Ocean App Platform Logo

Introduction

DigitalOcean App platform is a Platform-as-a-service (Paas) that allows developers to publish code directly to DigialOcean servers without worrying about the underlying infrastructure. The Platform supports two ways to build an image for your app: Cloud Native Buildpacks and Dockerfiles.

When an app is deployed to the App Platform either via GitHub or DockerHub, it defaults to using Dockerfile if one is present in the root directory or specified app spec. Otherwise, the App Platform checks your code to determine what language or framework it uses. If it supports the language or framework, it chooses an appropriate resource type and uses the proper buildpack to build the app and deploy a container.

Java/Spring boot is not supported out of the box on the App platform hence developers rely on a Dockerfile for deployment. This sometimes could be a daunting challenge as there is little documentation about it and it depends on the level of Docker expertise of the developer.

In this article, you'll learn how to deploy a Spring Boot app with a Docker to DigitalOcean App platform. You'll build a Spring Boot app that saves and retrieves messages from a postgres database, set instructions in the Dockerfile, push the code to a GitHub repository, then configure the application as a DigitalOcean app, attach a PostgreSQL database and deploy the app.

Prerequisites

For this tutorial, you'll need the following:

Step 1 — Create a Spring Boot Application

In this step, you’ll create a Spring Boot application using the Spring Boot CLI. You should explore the various installation options.

Note: If You encounter the eror below, please install OpenJDK 17

'LinkageError` mismatch (that the JRE was being set up with Java 11/bytecode 55 but needed to be run with Java 17/bytecode 61

To start, initialize a new Spring Boot project using the init command. The init command lets you create a new project by using the start.spring.io without leaving the shell, as shown below:

spring init --build=maven --java-version=17 --dependencies=web,data-jpa,postgresql spring-boot-docker   

The --build flag indicates the build type which could be maven or gradle. The --java-version flag indicates the preferred Java version. The --dependencies flag shows the list of project dependencies. This project uses the spring-boot-starter-web, spring-boot-starter-data-jpa and postgresql database driver.

Essentially, the above command creates a spring-boot-docker directory with a Maven-based project.

The output of the init command indicating the project creation is shown below:

Using service at https://start.spring.io
Project extracted to '/Users/your directory/spring-boot-docker'

You can list the capabalities of the Spring Boot CLI service by using the --list flag to list all dependencies as shown below.

spring init --list   

At this point, your project is set up in a new directory. Switch to the new directory:

cd spring-boot-docker

You have now created a springboot project and you are currently in the project folder. In the next section, you'll add a custom Java class managed by Spring Data JPA that will store data in the postgresql database.

Step 2 — Add Model Class

In this step, you'll create a Message Java class that will hold messages for our application. the Message class will contain two fields: Id and body.

Since you're in your project directory, navigate to the project root directory using this command:

cd src/main/java/com/example/springbootdocker

Create a new Message.java file:

touch Message.java

Open the Message.java file:

nano Message.java

Add the Message class to hold the id and body of the message:

package com.example.springbootdocker;

//import javax.persistence.*;
import jakarta.persistence.*;

@Entity
@Table(name = "message")
public class Message {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Column(name = "id", nullable = false)
    private Long id;
    private String body;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getBody() {
        return body;
    }

    public void setBody(String body) {
        this.body = body;
    }
}

The above is a simple Message.java class that has the Id and body as the only fields with their corresponding getter and setter methods.

If you are using Java 17, you need to change the import statement from import javax.persistence.*; to import jakarta.persistence.*.

The @Entity annotation specifies that the Message class is an entity and is mapped to a database table.

The @Id annotation specifies the primary key of the entity and the @GeneratedValue provides for the specification of generation strategies for the values of primary keys.

You have now created a Message class mapped to the postgres database that will hold messages. In the next step, you'll create a repository layer that will provide the mechanism for storage, retrieval, search, update and delete operations on the Message entity.

Step 3 — Add Repository Layer

In this step, you'll create a Java interface annotated with the @Repository annotation. This annotation is used to indicate that the class provides the mechanism for storage, retrieval, search, update and delete operations on the Message entity. For more details on @Repository annotation, see Spirng @Repository Annotation by Pankaj.

In your project directory, create a MessageRepository.java file:

touch MessageRepository.java

Open the MessageRepository.java file:

nano MessageRepository.java

Add the MessageRepository Interface which extends JpaRepository:

package com.example.springbootdocker;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;


@Repository
public interface MessageRepository extends JpaRepository<Message, Long> {}

The MessageRepository interface extends the JpaRepository which takes the Message class created in the previous step and the Long data type since the Id specified in the Message class is of type Long.

You have now created the repository layer for data accesss. In the next step, you'll create a controller layer that'll expose methods to save and retrieve messages via a RestFul webservice call.

Step 4 — Add Controller Layer

In this step, you'll create a spring boot controller class that will expose two methods to save and retrieve messages from the database.

Still in your project directory, create a MessageController file:

touch MessageController.java

Open the MessageController.java file:

nano MessageController.java

Add the MessageController classs which exposes two methods to save and retrieve messages.

package com.example.springbootdocker;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import java.util.Optional;

@RestController
@RequestMapping("/message")
public class MessageController {
 
    @Autowired
    MessageRepository messageRepository;

    @GetMapping("/{id}")
    public Optional<Message> getMessage(@PathVariable("id") Long id) {
        return messageRepository.findById(id);
    }

    @PostMapping
    public Message saveMessage(@RequestBody Message message) {
        return messageRepository.save(message);
    }

}

The @RestController annotation is a convenience annotation that is itself annotated with @Controller and @ResponseBody. This annotation is applied to a class to mark it as a request handler. Spring RestController annotation is used to create RESTful web services using Spring MVC. Spring RestController takes care of mapping request data to the defined request handler method. Once response body is generated from the handler method, it converts it to JSON or XML response. See Spring RestController by Pankaj for more details on @RestController.

So far, you have been able to create a Message Entity, Message Repository layer as well as the Message controller class. To be able to save and retrieve messages per the preceeding steps, you need to add some database configurations for the postgres database. You'll achieve this in the next step.

Note: You might need to change the import statements to match yours.

Step 5 — Add Postgres Database Configuration

In this step, you'll add database credentials to the application.properties file which was created in step 1. The database credentials ensures that a secure database connection can be made to the database.

Navigate to the resources directory from the project root directory using this command:

cd src/main/resources

Open the application.properties file.

nano application.properties

Add the following properties:

spring.datasource.url=jdbc:postgresql://localhost:5432/<changeme>
spring.datasource.username=<changeme>
spring.datasource.password=<changeme>
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true

Note: You need to change the credentials for your database.

The spring.datasource.url specifies the host, port and the database name. The spring.datasource.username refers to the name of your database and the spring.datasource.password refers to the password.

spring.jpa.properties.hibernate.dialect makes hibernate generate better SQL for the chosen database.

spring.jpa.hibernate.ddl-auto is a Hibernate feature that controls the behavior in a more fine-grained way. it could be create, create-drop, validate or update

spring.jpa.show-sql shows the sql in the logs for debugging.

In this step, you've added the necessary database configuration to the application.properties file. You're now ready to test your application locally. In the next step, you'll test your application.

Step 6 — Test Spring Boot App Locally

In this step, you'll test the save and retrieve methods exposed in the controller in step 4.

For this , you'll use curl command.

To run the app, navigate to the project root directory and run the command below:

 mvn spring-boot:run

The output of the command indicating that the project is running is shown below:

o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-11-24 08:29:03.159  INFO 50443 --- [           main] c.e.springbootdocker.DemoApplication     : Started DemoApplication in 1.399 seconds (JVM running for 1.526)

To save a message, run the command below in a new terminal shell:

curl -X POST http://localhost:8080/message -H 'Content-Type:  application/json' -d '{"body":"Hello world, I am making progress so far"}'

The above request saves a new message record to the database, the output is shown below:

{"id":1,"body":"Hello world, I am making progress so far"}%  

To retrieve a message by id, run the command below:

curl -X GET http://localhost:8080/message/1

The above request retrieves an existing message by id from the database, the output is shown below:

{"id":1,"body":"Hello world, I am making progress so far"}%  

So far, you have created an application that's working locally. In the next step, you'll create a Dockerfile that will handle the build and deployment process.

Step 7 — Add a Dockerfile

In this step, you'll add a Dockerfile to manage the build and deployment process.

Docker is an open-source platform for building, deploying, sharing and managing containerized applications. Developers can install the apps from a single package and get them up and running in minutes.

To Dockerize our Spring Boot App, a Dockerfile is used. The Dockerfileis a text document containing all the commands and instructions a user could call on the command line to assemble an image. A Docker image is composed of a stack of layers, each representing an instruction in our Dockerfile.

DigitalOcean App platform will rely on the Dockerfile for build instructions during deployment.

See this article for more information on Docker and this article for how to install Docker.

For this application, you'll create a Dockerfile in the app's root directory using the following command:

touch Dockerfile

Open the Dockerfile:

nano Dockerfile

Add the following commands to the Dockerfile in order to build the image for the application.

FROM eclipse-temurin:11-jdk-jammy as builder
WORKDIR /opt/app
COPY .mvn/ .mvn
COPY mvnw pom.xml ./
RUN ./mvnw dependency:go-offline
COPY ./src ./src
RUN ./mvnw clean install -DskipTests


FROM eclipse-temurin:11-jre-jammy
WORKDIR /opt/app
EXPOSE 8080
COPY --from=builder /opt/app/target/*.jar /opt/app/*.jar
ENTRYPOINT ["java","-Dspring.profiles.active=prod", "-jar", "/opt/app/*.jar" ]


The above Dockerfile uses a multi-stage build process by copying results from one image to another. The first stage extracts the dependencies while the second stage copies the extracted dependencies to the final image.

The command FROM eclipse-temurin:11-jdk-jammy as builder defines the base image from the Eclipse Temurin project, one of the most popular official images with a build-worthy JDK. The Eclipse Temurin project provides code and processes that support the building of runtime binaries and associated technologies.

Note: that Java 11 is specified for the app build, which matches the Java version in step 1

The second command WORKDIR /opt/app creates a working directory of a Docker container at any given time. Any RUN, CMD, ADD, COPY, or ENTRYPOINT command will be executed in the specified working directory.

COPY .mvn/ .mvn command copies files from a local source location to a destination in the Docker container.

RUN ./mvnw dependency:go-offline runs the spring boot application. The resulting committed image will be used for the next step in the Dockerfile. dependency:go-offline goal resolves all dependencies including plugins and reports and their dependencies. After running this goal, you can safely work in offline mode.

RUN ./mvnw clean install tells Maven to do the clean phase in each module before running the install phase for each module. This clears any compiled files you have, making sure that you're really compiling each module from scratch.

Notice that the first image is labelled builder. We use it to run eclipse-temurin:11-jdk-jammy, build the JAR, and unpack it.

The second layer of the multi-stage build process contain the build configuration and the source code for the application while the earlier layer contain the complete Eclipse JDK image. This small optimization saves us from copying the target directory to a Docker image.

Finally, an ENTRYPOINT lets you configure a container that runs as an executable. It corresponds to your java -jar target/*.jar command.

For more information on Dockerfile commands, see Docker Explained: Using Dockerfiles to Automate Building of Images by O.S Tezer.

So far you have created a Dockerfile that contains the necessary commands needed to build a Docker image. In the next step, you'll build the Docker image locally.

Step 8 — Build Docker Image

In this step, you'll build the Docker image locally based on the set of commands as specified in the Dockerfile.

To build a docker image, ensure your `Docker' instance is running before you run this command:

docker build -t spring-boot-docker .

The docker build command builds Docker images from the Dockerfile and a context. A build's context is the set of files locates in the specified PATH or URL.

The output of the build command indicating that the successful build of the Docker image is shown below:


[+] Building 846.9s (16/16) FINISHED                                                                                                                                                                                  
 => [internal] load build definition from Dockerfile                                                                                                                                                             0.0s
 => => transferring dockerfile: 37B                                                                                                                                                                              0.0s
 => [internal] load .dockerignore                                                                                                                                                                                0.0s
 => => transferring context: 2B                                                                                                                                                                                  0.0s
 => [internal] load metadata for docker.io/library/eclipse-temurin:11-jre-jammy                                                                                                                                  4.7s
 => [internal] load metadata for docker.io/library/eclipse-temurin:11-jdk-jammy                                                                                                                                  4.7s
 => [stage-1 1/3] FROM docker.io/library/eclipse-temurin:11-jre-jammy@sha256:94a23660199bda9133dff36ef811446d9866859946de3b6fea03ba3dca6618bc                                                                    0.0s
 => [internal] load build context                                                                                                                                                                                0.0s
 => => transferring context: 1.26kB                                                                                                                                                                              0.0s
 => [builder 1/7] FROM docker.io/library/eclipse-temurin:11-jdk-jammy@sha256:ff15d0a337affefa41a3619cb5d8626c11e0d38c84e85ac3b0fafc5f86ff68e9                                                                    0.0s
 => CACHED [stage-1 2/3] WORKDIR /opt/app                                                                                                                                                                        0.0s
 => CACHED [builder 2/7] WORKDIR /opt/app                                                                                                                                                                        0.0s
 => CACHED [builder 3/7] COPY .mvn/ .mvn                                                                                                                                                                         0.0s
 => CACHED [builder 4/7] COPY mvnw pom.xml ./                                                                                                                                                                    0.0s
 => [builder 5/7] RUN ./mvnw dependency:go-offline                                                                                                                                                             819.9s
 => [builder 6/7] COPY ./src ./src                                                                                                                                                                               0.0s
 => [builder 7/7] RUN ./mvnw clean install -DskipTests                                                                                                                                                          21.6s 
 => [stage-1 3/3] COPY --from=builder /opt/app/target/*.jar /opt/app/*.jar                                                                                                                                       0.1s 
 => exporting to image                                                                                                                                                                                           0.1s 
 => => exporting layers                                                                                                                                                                                          0.1s 
 => => writing image sha256:0f5e9737106234ca975fd9b38e50e5e461872eb959b8fde1854adbfebb74b415                                                                                                                     0.0s 
 => => naming to docker.io/library/spring-boot-docker                                                                                                                                                            0.0s                                                        

In this step, you have successfully built a Docker image locally. In the next step, you'll push the app to a GitHub repository.

Step 9 — Push Code to GitHub

To deploy your app, App platform retrieves your source code from a specified Github repository.

Login to your GitHub acocunt and create a new public or private repository called spring-boot-docker. This automatically initializes the repository with git which enable you to push the code directky to GitHub.

To initialize the local repository, add the command:

git init

You'll need to add the repository to your local project using this command:

git remote add origin https://github.com/your_name/spring-boot-docker

Next, switch to the main branch using the following command:

git branch -M main

Add all the project files to git using this command:

git add .

After adding the files, commit the files using:

git commit -m "commit message"

Finally, push to code to your Github repository using this command:

git push -u origin main

Once prompted, enter your GitHub credentials. If done successfully, you'll receive a success message similar to:


Enumerating objects: 29, done.
Counting objects: 100% (29/29), done.
Delta compression using up to 10 threads
Compressing objects: 100% (21/21), done.
Writing objects: 100% (29/29), 60.07 KiB | 15.02 MiB/s, done.
Total 29 (delta 0), reused 0 (delta 0), pack-reused 0
To https://github.com/Ikhiloya/spring-boot-docker.git
 * [new branch]      main -> main
Branch 'main' set up to track remote branch 'main' from 'origin'.

In this step, you created a GitHub repository and pushed your code to that repository. In the next step, you'll create a DigitalOcean app and deploy from your GitHub repository.

Step 10 —Deploy App to DigitalOcean App Platform

In this step, you'll learn how to deploy your project from the GitHub repository to DigitalOcean App platform.

To start, login to your DigitalOcean account, click on the Create button and select Apps from the drop down menu:

Create App screen on DigitalOcean App Platform

You'll be prompted to link your preferred GitHub repository which will require you to authorize DigitalOcean to be able to access your repositories. Choose the spring-boot-docker repository you created earlier and click on next as shown below:

Create Resource from source code

You'll notice that App platform has detected the Dockerfile from the project repository. You can choose Edit plan if you want to change the plan.

Click Next to proceed to add Environment Variables which enables all resources to have access to variables at build time and runtime.

For now, you don't need to add anything to the Environment variables as you have not attached a database which will be discussed later.

Click Next to select your preferred app name and region that's closest to your physical location.

Click Next again to review your app's resources after which you can click on Create Resources to get your app running.

Review App Resources

This effectively deploys the application using the Dockerfile you created. Since you have not attached a database, you'll have a database connection error:

org.postgresql.util.PSQLException: connection to localhost:5432 refused .....

To fix this error, you need to attach a database and add the database credentials as environment variables.

Step 11 —Attach a Postgresgl Database

In this step, you'll attach a database and add the database connection parameters as Environment variables. This will effectively eliminate the database connection error observed in the previous step.

Click the Create button and select Database; select a postgres database and your preferred plan.

Click Next to view the database credentials.

You need to add the database credentials as environment variables as key-value pairs. Navigate to the Settings tab of your app, click on the app component and edit the Environment Variables with the following:

SPRING_DATASOURCE_URL=jdbc:postgresql://<host>:<port>/<dbname>?sslmode=require
SPRING_DATASOURCE_USERNAME=<changeme>
SPRING_DATASOURCE_PASSWORD=<changeme>

Add Environment Variables

Click on save and this will trigger the deployment process. Once the deployment is successful, you'll see an output like this:

o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2022-11-24 08:29:03.159  INFO 50443 --- [           main] c.e.springbootdocker.DemoApplication     : Started DemoApplication in 1.399 seconds (JVM running for 1.526)

In this step you have learnt how to attach a database and add the database credentials as environment variables. In the next step, you'll be able to test using the live url.

Step 12 —Test App using Live Url

In this step, you'll obtain the app's live url and test the endpoints to save and retrieve messages.

Click on the Apps tab, navigate to your app and copy the app's live url.

To save a message, use the following command:

curl -X POST https://your-app-url/message -H 'Content-Type:  application/json' -d '{"body":"Hello world, I am making progress so far"}' 

The above request saves a new message record to the database, the output is shown below:

{"id":4,"body":"Hello world, I am making progress so far"}%  

To retrieve a message by id, run the command below:

curl -X GET https://your-app-url/message/4

The above request retrieves an existing message by id from the database, the output is shown below:

{"id":4,"body":"Hello world, I am making progress so far"}%  

So far, you have been able to test the app using the app's live url.

Conclusion

In this tutorial, you have learnt how to deploy a Spring Boot App using a Dockerfile to DigitalOcean App platform, attach a database and test using the live url.

You can find the source code for this project in this GitHub repository.

For further information, please visit the official App Platform product documentation.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment