Deploying with Java (Spring Boot)
Overview
There are three steps to deploy your application:
- Containerize your application by creating a 
Dockerfile - Modify 
devfile.yamlto add your Kubernetes code - Run 
odo deploy 
Prerequisites
In order to use odo deploy, you must be able to build an image as well as push to a registry.
Step 1. Let odo know where to push container images
odo needs to know where to push non-absolute container images declared in the devfile.yaml file.
You can configure odo with the odo preference set command, like so:
odo preference set ImageRegistry $registry
Example Output
$ odo preference set ImageRegistry ttl.sh
 ✓  Value of 'imageregistry' preference was set to 'ttl.sh'
Step 2 (Optional). Login to your container registry
If the container registry you registered requires some form of authentication, you will need to login to it.
Note that the cluster you are deploying to also needs to be able to pull images from this registry in order for the application container to be started properly.
- Podman
 - Docker
 
podman login $registry
Example Output
$ podman login quay.io
Username:
Password:
Login Succeeded!
docker login $registry
Example Output
$ docker login docker.io
Username:
Password:
Login Succeeded!
Step 3. Set the appropriate container build platform
Your container image build must match the same architecture as the cluster you are deploying to.
For example: you will have to cross-build a AMD64 image on a Mac M1 (ARM64) in order to deploy to a AMD64 cluster.
odo allows you to do so via the ODO_IMAGE_BUILD_ARGS environment variable,
which is a semicolon-separated list of extra arguments to pass to Podman or Docker when building images.
Choose your deployment architecture:
- Linux (AMD64)
 - Linux (ARM)
 - Windows (AMD64)
 
export ODO_IMAGE_BUILD_ARGS="--platform=linux/amd64"
export ODO_IMAGE_BUILD_ARGS="--platform=linux/arm64"
export ODO_IMAGE_BUILD_ARGS="--platform=windows/amd64"
Step 1. Create the initial development application
Complete the Developing with Java (Spring Boot) guide before continuing.
Step 2. Containerize the application
In order to deploy our application, we must containerize it in order to build and push to a registry. Create the following Dockerfile in the same directory:
FROM registry.access.redhat.com/ubi8/openjdk-11 as builder
USER jboss
WORKDIR /tmp/src
COPY  . /tmp/src
RUN mvn package
FROM registry.access.redhat.com/ubi8/openjdk-11
COPY  /tmp/src/target/*.jar /deployments/app.jar
Step 3. Modify the Devfile
Let's modify the devfile.yaml and add the respective deployment code.
When copy/pasting to devfile.yaml, make sure the lines you inserted are correctly indented.
odo deploy uses Devfile schema 2.2.0. Change the schema to reflect the change:
# Deploy "kind" ID's use schema 2.2.0+
schemaVersion: 2.2.0
Add the variables and change them appropriately:
# Add the following variables section anywhere in devfile.yaml
variables:
  APP_NAME: my-java-app
  CONTAINER_PORT: "8080"
  # The ingress domain name is not necessary when deploying to an OpenShift Cluster.
  # OpenShift will provide us with a dynamic URL to access the application.
  DOMAIN_NAME: java.example.com
Add the commands used to deploy:
# This is the main "composite" command that will run all below commands
commands:
- id: deploy
  composite:
    commands:
    - build-image
    - k8s-deployment
    - k8s-service
    - k8s-url
    group:
      isDefault: true
      kind: deploy
# Below are the commands and their respective components that they are "linked" to deploy
- id: build-image
  apply:
    component: outerloop-build
- id: k8s-deployment
  apply:
    component: outerloop-deployment
- id: k8s-service
  apply:
    component: outerloop-service
- id: k8s-url
  apply:
    component: outerloop-url
Add the Docker image location as well as Kubernetes Deployment and Service resources to components:
components:
# This will build the container image before deployment
- name: outerloop-build
  image:
    dockerfile:
      buildContext: ${PROJECT_SOURCE}
      rootRequired: false
      uri: ./Dockerfile
    imageName: "{{APP_NAME}}"
# This will create a Deployment in order to run your container image across
# the cluster.
- name: outerloop-deployment
  kubernetes:
    inlined: |
      kind: Deployment
      apiVersion: apps/v1
      metadata:
        name: {{APP_NAME}}
      spec:
        replicas: 1
        selector:
          matchLabels:
            app: {{APP_NAME}}
        template:
          metadata:
            labels:
              app: {{APP_NAME}}
          spec:
            containers:
              - name: {{APP_NAME}}
                image: {{APP_NAME}}
                ports:
                  - name: http
                    containerPort: {{CONTAINER_PORT}}
                    protocol: TCP
                resources:
                  limits:
                    memory: "1024Mi"
                    cpu: "500m"
# This will create a Service so your Deployment is accessible.
# Depending on your cluster, you may modify this code so it's a
# NodePort, ClusterIP or a LoadBalancer service.
- name: outerloop-service
  kubernetes:
    inlined: |
      apiVersion: v1
      kind: Service
      metadata:
        name: {{APP_NAME}}
      spec:
        ports:
        - name: "{{CONTAINER_PORT}}"
          port: {{CONTAINER_PORT}}
          protocol: TCP
          targetPort: {{CONTAINER_PORT}}
        selector:
          app: {{APP_NAME}}
        type: NodePort
To be able to access our application we need to add one more component to the Devfile.
For OpenShift cluster we add Route. For Kubernetes cluster we add Ingress.
- Kubernetes
 - OpenShift
 
- name: outerloop-url
  kubernetes:
    inlined: |
      apiVersion: networking.k8s.io/v1
      kind: Ingress
      metadata:
        name: {{APP_NAME}}
      spec:
        rules:
          - host: "{{DOMAIN_NAME}}"
            http:
              paths:
                - path: "/"
                  pathType: Prefix
                  backend:
                    service:
                      name: {{APP_NAME}}
                      port:
                        number: {{CONTAINER_PORT}}
- name: outerloop-url
  kubernetes:
    inlined: |
      apiVersion: route.openshift.io/v1
      kind: Route
      metadata:
        name: {{APP_NAME}}
      spec:
        path: /
        to:
          kind: Service
          name: {{APP_NAME}}
        port:
          targetPort: {{CONTAINER_PORT}}
Your final Devfile should look something like this:
Your Devfile might slightly vary from the example below, but the example should give you an idea about the placements of all the components and commands.
- Kubernetes
 - OpenShift
 
commands:
- exec:
    commandLine: mvn clean -Dmaven.repo.local=/home/user/.m2/repository package -Dmaven.test.skip=true
    component: tools
    group:
      isDefault: true
      kind: build
    workingDir: ${PROJECT_SOURCE}
  id: build
- exec:
    commandLine: mvn -Dmaven.repo.local=/home/user/.m2/repository spring-boot:run
    component: tools
    group:
      isDefault: true
      kind: run
    workingDir: ${PROJECT_SOURCE}
  id: run
- exec:
    commandLine: java -Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=${DEBUG_PORT},suspend=n
      -jar target/*.jar
    component: tools
    group:
      isDefault: true
      kind: debug
    workingDir: ${PROJECT_SOURCE}
  id: debug
# This is the main "composite" command that will run all below commands
- id: deploy
  composite:
    commands:
    - build-image
    - k8s-deployment
    - k8s-service
    - k8s-url
    group:
      isDefault: true
      kind: deploy
# Below are the commands and their respective components that they are "linked" to deploy
- id: build-image
  apply:
    component: outerloop-build
- id: k8s-deployment
  apply:
    component: outerloop-deployment
- id: k8s-service
  apply:
    component: outerloop-service
- id: k8s-url
  apply:
    component: outerloop-url
components:
- container:
    command:
    - tail
    - -f
    - /dev/null
    endpoints:
    - name: http-springboot
      targetPort: 8080
    - exposure: none
      name: debug
      targetPort: 5858
    env:
    - name: DEBUG_PORT
      value: "5858"
    image: registry.access.redhat.com/ubi8/openjdk-11:latest
    memoryLimit: 768Mi
    mountSources: true
    volumeMounts:
    - name: m2
      path: /home/user/.m2
  name: tools
- name: m2
  volume:
    size: 3Gi
# This will build the container image before deployment
- name: outerloop-build
  image:
    dockerfile:
      buildContext: ${PROJECT_SOURCE}
      rootRequired: false
      uri: ./Dockerfile
    imageName: "{{APP_NAME}}"
# This will create a Deployment in order to run your container image across
# the cluster.
- name: outerloop-deployment
  kubernetes:
    inlined: |
      kind: Deployment
      apiVersion: apps/v1
      metadata:
        name: {{APP_NAME}}
      spec:
        replicas: 1
        selector:
          matchLabels:
            app: {{APP_NAME}}
        template:
          metadata:
            labels:
              app: {{APP_NAME}}
          spec:
            containers:
              - name: {{APP_NAME}}
                image: {{APP_NAME}}
                ports:
                  - name: http
                    containerPort: {{CONTAINER_PORT}}
                    protocol: TCP
                resources:
                  limits:
                    memory: "1024Mi"
                    cpu: "500m"
# This will create a Service so your Deployment is accessible.
# Depending on your cluster, you may modify this code so it's a
# NodePort, ClusterIP or a LoadBalancer service.
- name: outerloop-service
  kubernetes:
    inlined: |
      apiVersion: v1
      kind: Service
      metadata:
        name: {{APP_NAME}}
      spec:
        ports:
        - name: "{{CONTAINER_PORT}}"
          port: {{CONTAINER_PORT}}
          protocol: TCP
          targetPort: {{CONTAINER_PORT}}
        selector:
          app: {{APP_NAME}}
        type: NodePort
- name: outerloop-url
  kubernetes:
    inlined: |
      apiVersion: networking.k8s.io/v1
      kind: Ingress
      metadata:
        name: {{APP_NAME}}
      spec:
        rules:
          - host: "{{DOMAIN_NAME}}"
            http:
              paths:
                - path: "/"
                  pathType: Prefix
                  backend:
                    service:
                      name: {{APP_NAME}}
                      port:
                        number: {{CONTAINER_PORT}}
metadata:
  description: Spring Boot® using Java
  displayName: Spring Boot®
  globalMemoryLimit: 2674Mi
  icon: https://spring.io/images/projects/spring-edf462fec682b9d48cf628eaf9e19521.svg
  language: Java
  name: my-java-app
  projectType: springboot
  tags:
  - Java
  - Spring Boot
  version: 1.2.0
schemaVersion: 2.2.0
starterProjects:
- git:
    remotes:
      origin: https://github.com/odo-devfiles/springboot-ex.git
  name: springbootproject
variables:
  APP_NAME: my-java-app
  CONTAINER_PORT: "8080"
  DOMAIN_NAME: java.example.com
commands:
- exec:
    commandLine: mvn clean -Dmaven.repo.local=/home/user/.m2/repository package -Dmaven.test.skip=true
    component: tools
    group:
      isDefault: true
      kind: build
    workingDir: ${PROJECT_SOURCE}
  id: build
- exec:
    commandLine: mvn -Dmaven.repo.local=/home/user/.m2/repository spring-boot:run
    component: tools
    group:
      isDefault: true
      kind: run
    workingDir: ${PROJECT_SOURCE}
  id: run
- exec:
    commandLine: java -Xdebug -Xrunjdwp:server=y,transport=dt_socket,address=${DEBUG_PORT},suspend=n
      -jar target/*.jar
    component: tools
    group:
      isDefault: true
      kind: debug
    workingDir: ${PROJECT_SOURCE}
  id: debug
# This is the main "composite" command that will run all below commands
- id: deploy
  composite:
    commands:
    - build-image
    - k8s-deployment
    - k8s-service
    - k8s-url
    group:
      isDefault: true
      kind: deploy
# Below are the commands and their respective components that they are "linked" to deploy
- id: build-image
  apply:
    component: outerloop-build
- id: k8s-deployment
  apply:
    component: outerloop-deployment
- id: k8s-service
  apply:
    component: outerloop-service
- id: k8s-url
  apply:
    component: outerloop-url
components:
- container:
    command:
    - tail
    - -f
    - /dev/null
    endpoints:
    - name: http-springboot
      targetPort: 8080
    - exposure: none
      name: debug
      targetPort: 5858
    env:
    - name: DEBUG_PORT
      value: "5858"
    image: registry.access.redhat.com/ubi8/openjdk-11:latest
    memoryLimit: 768Mi
    mountSources: true
    volumeMounts:
    - name: m2
      path: /home/user/.m2
  name: tools
- name: m2
  volume:
    size: 3Gi
# This will build the container image before deployment
- name: outerloop-build
  image:
    dockerfile:
      buildContext: ${PROJECT_SOURCE}
      rootRequired: false
      uri: ./Dockerfile
    imageName: "{{APP_NAME}}"
# This will create a Deployment in order to run your container image across
# the cluster.
- name: outerloop-deployment
  kubernetes:
    inlined: |
      kind: Deployment
      apiVersion: apps/v1
      metadata:
        name: {{APP_NAME}}
      spec:
        replicas: 1
        selector:
          matchLabels:
            app: {{APP_NAME}}
        template:
          metadata:
            labels:
              app: {{APP_NAME}}
          spec:
            containers:
              - name: {{APP_NAME}}
                image: {{APP_NAME}}
                ports:
                  - name: http
                    containerPort: {{CONTAINER_PORT}}
                    protocol: TCP
                resources:
                  limits:
                    memory: "1024Mi"
                    cpu: "500m"
# This will create a Service so your Deployment is accessible.
# Depending on your cluster, you may modify this code so it's a
# NodePort, ClusterIP or a LoadBalancer service.
- name: outerloop-service
  kubernetes:
    inlined: |
      apiVersion: v1
      kind: Service
      metadata:
        name: {{APP_NAME}}
      spec:
        ports:
        - name: "{{CONTAINER_PORT}}"
          port: {{CONTAINER_PORT}}
          protocol: TCP
          targetPort: {{CONTAINER_PORT}}
        selector:
          app: {{APP_NAME}}
        type: NodePort
- name: outerloop-url
  kubernetes:
    inlined: |
      apiVersion: route.openshift.io/v1
      kind: Route
      metadata:
        name: {{APP_NAME}}
      spec:
        path: /
        to:
          kind: Service
          name: {{APP_NAME}}
        port:
          targetPort: {{CONTAINER_PORT}}
metadata:
  description: Spring Boot® using Java
  displayName: Spring Boot®
  globalMemoryLimit: 2674Mi
  icon: https://spring.io/images/projects/spring-edf462fec682b9d48cf628eaf9e19521.svg
  language: Java
  name: my-java-app
  projectType: springboot
  tags:
  - Java
  - Spring Boot
  version: 1.2.0
schemaVersion: 2.2.0
starterProjects:
- git:
    remotes:
      origin: https://github.com/odo-devfiles/springboot-ex.git
  name: springbootproject
# Add the following variables code anywhere in devfile.yaml
variables:
  APP_NAME: my-java-app
  CONTAINER_PORT: "8080"
Step 4. Run the odo deploy command
Now we're ready to deploy our application on the cluster:
odo deploy
Sample Output
$ odo deploy
  __
 /  \__     Deploying the application using my-java-app Devfile
 \__/  \    Namespace: odo-dev
 /  \__/    odo version: v3.15.0
 \__/
↪ Building & Pushing Container: ttl.sh/my-java-app-my-java-app:611242
 •  Building image locally  ...
build -t ttl.sh/my-java-app-my-java-app -f /home/user/quickstart-demo/java-demo/Dockerfile /home/user/quickstart-demo/java-demo
 ✓  Building image locally [5ms]
 •  Pushing image to container registry  ...
push ttl.sh/my-java-app-my-java-app
 ✓  Pushing image to container registry
↪ Deploying Kubernetes Component: my-java-app
 ✓  Creating resource Deployment/my-java-app
↪ Deploying Kubernetes Component: my-java-app
 ✓  Creating resource Service/my-java-app
↪ Deploying Kubernetes Component: my-java-app
 ✓  Creating resource Ingress/my-java-app
Your Devfile has been successfully deployed
If you are using the quay.io registry, you might have to change the permissions of the newly pushed image to Public to continue. Otherwise, you might see failures related to pulling the image.
Step 5. Accessing the application
You can now determine how to access the application by running odo describe component:
odo describe component
Check for Kubernetes Ingresses if you are on a Kubernetes cluster or OpenShift Routes if you are an OpenShift cluster to obtain the URI for accessing your application.
Sample Output
- Kubernetes
 - OpenShift
 
$ odo describe component
Name: my-java-app
Display Name: Spring Boot®
Project Type: springboot
Language: Java
Version: 1.2.0
Description: Spring Boot® using Java
Tags: Java, Spring Boot
Running in: Deploy
Supported odo features:
 •  Dev: true
 •  Deploy: true
 •  Debug: true
Container components:
 •  tools
Kubernetes components:
 •  outerloop-deployment
 •  outerloop-service
 •  outerloop-url
Kubernetes Ingresses:
 •  my-java-app: java.example.com/
$ odo describe component
Name: my-java-app
Display Name: Spring Boot®
Project Type: springboot
Language: Java
Version: 1.2.0
Description: Spring Boot® using Java
Tags: Java, Spring Boot
Running in: Deploy
Supported odo features:
 •  Dev: true
 •  Deploy: true
 •  Debug: true
Container components:
 •  tools
Kubernetes components:
 •  outerloop-deployment
 •  outerloop-service
 •  outerloop-url
OpenShift Routes:
 •  my-java-app: my-java-app-user-crt-dev.apps.sandbox-m2.ll9k.p1.openshiftapps.com/
- Kubernetes
 - OpenShift
 
Since we are using Ingress, we can check if an IP address has been set.
$ kubectl get ingress my-java-app
NAME            CLASS     HOSTS                ADDRESS      PORTS   AGE
my-java-app   traefik   java.example.com   172.19.0.2   80      2m2s
Once the IP address appears, you can now access the application, like so:
curl --resolve "java.example.com:80:172.19.0.2" -i http://java.example.com/
We can directly access the application by using the OpenShift Route displayed in the odo describe component output above:
curl -i http://my-java-app-user-crt-dev.apps.sandbox-m2.ll9k.p1.openshiftapps.com/
Step 6. Delete the resources
After testing your application, you may optionally undeploy using the odo delete component command:
odo delete component
Sample output
$ odo delete component
Searching resources to delete, please wait...
This will delete "my-java-app" from the namespace "odo-dev".
 •  The following resources will get deleted from cluster:
 •      - Deployment: my-java-app
 •      - Service: my-java-app
 •      - Ingress: my-java-app
? Are you sure you want to delete "my-java-app" and all its resources? Yes
 ✓  Deleting resources from cluster [75ms]
The component "my-java-app" is successfully deleted from namespace "odo-dev"