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.
For this tutorial, you'll need the following:
-
Java 8 version or greater installed on your local machine.
-
Git installed on your local machine. You can follow the tutorial Contributing to Open Source: Getting Started with Git to install and set up Git on your local computer.
-
A local machine with Docker installed. Your local machine can be running any Linux distribution, or even Windows or macOS. For Windows and macOS, install Docker using the official installer. If you have Ubuntu 16.04 running on your local machine, but Docker is not installed, see How To Install and Use Docker on Ubuntu 16.04 for instructions.
-
A local machine with Maven installed. Your local machine can be running any Linux distribution, or even Windows or macOS. For macOS, See How to install Maven on Mac OS by Pankaj. For Linux See How to install Maven on Linux (Ubuntu) by Pankaj.
-
Spring Boot Command Line Tool which you can use to quickly prototype with spring without an IDE. See Installing the Spring Boot CLI for details on how to install the CLI, Maven, Gradle and an spring boot example.
-
Spring Boot based development projects by reading Spring Boot Tutorial by Rambabu Posa.
-
Spring Data JPA by reading Spring Data JPA by Pankaj
-
Postgresql Database by reading What is PostgreSQL by Mark Drake. See How To Install and Use PostgreSQL for details on how to install and use PostgreSQL.
-
A DigitalOcean account.
-
An account on GitHub, which you can create by going to the Create your Account page.
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.
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.
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.
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.
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.
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.
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 Dockerfile
is 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.
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.
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.
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:
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:
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.
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.
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>
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.
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.
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.