{{announcement.body}}
{{announcement.title}}

Angular, Docker, and Spring Boot: A Match Made in Heaven

DZone 's Guide to

Angular, Docker, and Spring Boot: A Match Made in Heaven

Here we take a look at the combination of Angular, Docker, and Spring Boot in a sample application with this tutorial and screencast.

· Web Dev Zone ·
Free Resource

Angular and Spring Boot are extremely popular, and used often while creating modern apps. When looking for popular search terms in Google's Keyword Planner, these two together appear often. What comes out of this is a summary of monthly searches, which are unlimited by location, and averaged.

A series of tutorials on Angular and Spring Boot has led to this fourth and final tutorial. Throughout this series one is taught how to use Angular and Spring Boot + Kotlin in order to create a more secure notes app, how to add Bootstrap for CSS, as well as making it easy to interact with data tables. Second to last, the tutorials show how to deploy apps separately to  Heroku. In addition to this, the tutorial shows how to use the ng deploy to deploy AWS, Firebase, and Netlify. Tutorials are linked below. 

  1. Build a CRUD App with Angular 9 and Spring Boot 2.2
  2. Build Beautiful Angular Apps with Bootstrap
  3. Angular Deployment with a Side of Spring Boot

In this tutorial, I will be outlining the use of Docker in order to create images for Angular, and deploy said images to Heroku, how to use both Angular and Spring Boot into the same JAR artifact for deployment. I will also include how to Dockerize combined clips using Cloud Native Buildpacks and Jib, and how to deploy personal Docker images to Cloud Foundry, Knative on Google Cloud, and Heroku.

The most popular way to build and share containers is by far Docker. 'Dockerizing' is when you package your app, which can also involve adding web servers to your app. When containerizing an Angular app, this can become very important because an Angular app only has its artifacts as JavaScript, CSS, and HTML, which means that the product app will need to serve up its static files by a web server. If you are worried about security, the webserver has the ability to send security headers by configuring it, making it much safer for users.

Table of Contents

I’d like to thank many folks for their help with this post. Benoit Sautel for help with Gradle + Kotlin, Ray Tsang for help with Jib, James Ward for help with Knative, Josh Long for his assistance with Docker + Cloud Foundry, and Joe Kutner for his buildpacks support. Y’all rock! ��

You can also watch this tutorial as a screencast!


Create an Angular + Spring Boot App

To begin, clone the GitHub repo from the most recent tutorial in this series.

Shell
 




x


 
1
git clone https://github.com/oktadeveloper/okta-angular-deployment-example.git \
2
 angular-spring-boot-docker


   

Prerequisites:

Secure Your Angular + Spring Boot App with OpenID Connect

OAuth 2.0 was finalized in 2012 and has since become the industry-standard protocol for authorization. In 2014, OpenID Connect (OIDC) extended OAuth, adding federated identity to delegated authorization. Together, these two layers offer a standard specification that developers can write code against in a way that will work across multiple identity providers.

To begin the process of adding OIDC to Angular and Spring Boot, you’ll need to create a Heroku account. If you already have a Heroku account, log into it. Once you’re logged in, create a new app. I named mine bootiful-angular.

After creating your app, click on the Resources tab and add the Okta add-on.

If you haven’t entered a credit card for your Heroku account, you will get an error. This is because Heroku requires you to have a credit card on file to use any of their add-ons, even free ones. This is part of Heroku’s assurance to guard against misuse (real person, real credit card, etc.). I think this is a good security practice. Add a credit card to continue.

Click Provision and wait 20-30 seconds while your Okta account is created and OIDC apps are registered. Now go to your app’s Settings tab and click the Reveal Config Vars button. The variables displayed are the environment variables you can use to configure both Angular and Spring Boot for OIDC authentication.

Create an okta.env file in the angular-spring-boot-docker/notes-api directory and copy the variable values into it, where $OKTA_* is the value from Heroku.

Shell
 




xxxxxxxxxx
1


 
1
export OKTA_OAUTH2_ISSUER=$OKTA_OAUTH2_ISSUER
2
export OKTA_OAUTH2_CLIENT_ID=$OKTA_OAUTH2_CLIENT_ID_WEB
3
export OKTA_OAUTH2_CLIENT_SECRET=$OKTA_OAUTH2_CLIENT_SECRET_WEB


If you’re on Windows without Windows Subsystem for Linux installed, create an okta.bat file and use SET instead of export.

Start your Spring Boot app by navigating to the notes-api directory, sourcing this file, and running bootRun.

Shell
 




xxxxxxxxxx
1


 
1
source okta.env
2
./gradlew bootRun


For Windows users, the commands will be:

Shell
 




xxxxxxxxxx
1


1
okta.bat
2
gradlew bootRun


Next, configure Angular for OIDC authentication by modifying its auth-routing.module.ts to use the generated issuer and SPA client ID.

notes/src/app/auth-routing.module.ts

TypeScript
 




xxxxxxxxxx
1


1
const oktaConfig = {
2
  issuer: '$OKTA_OAUTH2_ISSUER',
3
  redirectUri: window.location.origin + '/callback',
4
  clientId: '$OKTA_OAUTH2_CLIENT_ID_SPA',
5
  pkce: true
6
};


Install your Angular app’s dependencies and start it.

Shell
 




xxxxxxxxxx
1


 
1
npm i
2
ng serve


Open http://localhost:4200 in your browser.

Click the Login button in the top right corner. You should be logged in without seeing a login form because you’re already logged in to Okta. If you want to see the full authentication flow, log out, or try it in a private window. You can use the $OKTA_ADMIN_EMAIL and $OKTA_ADMIN_PASSWORD from your Heroku config variables for credentials. Create a note to make sure everything works.

Commit your progress to Git from the top-level angular-spring-boot-docker directory.

Shell
 




xxxxxxxxxx
1


1
git commit -am "Add Okta OIDC Configuration"


Create a Docker Container for Your Angular App

Create a notes/Dockerfile that uses Node and Nginx as a web server.

Dockerfile
 




xxxxxxxxxx
1
14


 
1
FROM node:14.1-alpine AS builder
2
 
          
3
WORKDIR /opt/web
4
COPY package.json package-lock.json ./
5
RUN npm install
6
 
          
7
ENV PATH="./node_modules/.bin:$PATH"
8
 
          
9
COPY . ./
10
RUN ng build --prod
11
 
          
12
FROM nginx:1.17-alpine
13
COPY nginx.config /etc/nginx/conf.d/default.conf
14
COPY --from=builder /opt/web/dist/notes /usr/share/nginx/html


When I was trying to get everything to work, I found it handy to comment out the RUN ng build --prod line and use the following instead.

Shell
 




xxxxxxxxxx
1


 
1
RUN mkdir -p dist/notes
2
RUN echo "Hello, World" > dist/notes/index.html


This allows you to skip the lengthy Angular build process.

This will build your project and add Nginx as a web server. You’ll need to create the nginx.config file to make Nginx SPA-aware.

notes/nginx.config

Java
 




xxxxxxxxxx
1
11


 
1
server {
2
    listen   80;
3
    server_name  _;
4
 
          
5
    root /usr/share/nginx/html;
6
    index index.html;
7
 
          
8
    location / {
9
        try_files $uri /index.html;
10
    }
11
}


Make sure your Docker daemon is running with docker ps. Then run the following command to build your Docker image. The ng-notes value can be whatever you want to name your image.

Shell
 




xxxxxxxxxx
1


 
1
docker build -t ng-notes .


If it builds successfully, you’ll see messages like the following:

Shell
 




xxxxxxxxxx
1


 
1
Successfully built 382b9cd7d345
2
Successfully tagged ng-notes:latest


You can run it locally on port 4200 using the docker run command.

Shell
 




xxxxxxxxxx
1


 
1
docker run -p 4200:80 ng-notes


Add these Docker commands as scripts to your package.json file.

JSON
 




xxxxxxxxxx
1


 
1
"docker": "docker build -t ng-notes .",
2
"ng-notes": "docker run -p 4200:80 ng-notes"


The docker run command will serve up the production version of the Angular app, which has its backend configured to point to a production URL on Heroku.

notes/src/environments/environment.prod.ts

TypeScript
 




xxxxxxxxxx
1


1
export const environment = {
2
  production: true,
3
  apiUrl: 'https://bootiful-angular.herokuapp.com'
4
};


You’ll need to deploy your Spring Boot app to a similar public URL for your Angular + Docker container to work in production.


If you already deployed Spring Boot to Heroku (from the last tutorial), you can skip the next section and go straight to deploying your Angular Docker container to Heroku.

Deploy Spring Boot to Heroku

One of the easiest ways to interact with Heroku is with the Heroku CLI. Install it before proceeding with the instructions below.

Shell
 




xxxxxxxxxx
1


 
1
brew tap heroku/brew && brew install heroku


Open a terminal and log in to your Heroku account.

Shell
 




xxxxxxxxxx
1


1
heroku login


You should already have a Heroku app that you’ve added Okta to. You can use it for hosting your Spring Boot app. Run heroku apps and you’ll see the one that you created.

Shell
 




xxxxxxxxxx
1


 
1
$ heroku apps
2
=== matt.raible@okta.com Apps
3
bootiful-angular


You can run heroku config -a $APP_NAME to see your Okta variables. In my case, I’ll be using bootiful-angular for $APP_NAME.

Associate your existing Git repo with the app on Heroku.

Shell
 




xxxxxxxxxx
1


 
1
heroku git:remote -a $APP_NAME


Set the APP_BASE config variable to point to the notes-api directory. While you’re there, add the monorepo and Gradle buildpacks.

Shell
 




xxxxxxxxxx
1


 
1
heroku config:set APP_BASE=notes-api
2
heroku buildpacks:add https://github.com/lstoll/heroku-buildpack-monorepo
3
heroku buildpacks:add heroku/gradle


Attach a PostgreSQL database to your app.

Shell
 




xxxxxxxxxx
1


1
heroku addons:create heroku-postgresql


By default, Heroku’s Gradle support runs ./gradlew build -x test. Since you want it to run ./gradlew bootJar -Pprod, you’ll need to override it by setting a GRADLE_TASK config var.

Shell
 




xxxxxxxxxx
1


1
heroku config:set GRADLE_TASK="bootJar -Pprod"


The $OKTA_* environment variables don’t have the same names as the Okta Spring Boot starter expects. This is because the Okta Heroku Add-On creates two apps: a SPA and a web app. The web app’s config variables end in _WEB. You’ll have to make some changes so those variables are used for the Okta Spring Boot starter. Run the following command and remove _WEB from the two variables that have it.

Shell
 




xxxxxxxxxx
1


 
1
heroku config:edit


Now you’re ready to deploy! Heroku makes this easy with a simple git push.

Shell
 




xxxxxxxxxx
1


 
1
git push heroku main:master


By default, JPA is configured to create your database schema each time. Change it to simply validate the schema.

Shell
 




xxxxxxxxxx
1


 
1
heroku config:set SPRING_JPA_HIBERNATE_DDL_AUTO=validate


Now, you’ll need to configure your Angular app to use your Heroku-deployed Spring Boot app for its production URL.

notes/src/environments/environment.prod.ts

TypeScript
 




xxxxxxxxxx
1


 
1
export const environment = {
2
  production: true,
3
  apiUrl: 'https://<your-heroku-app>.herokuapp.com'
4
};


Since this runs the production build, you’ll need to add http://localhost:4200 as an allowed origin in your Spring Boot app on Heroku. Run the following command and add it to the end of the existing values.

Shell
 




xxxxxxxxxx
1


 
1
heroku config:set ALLOWED_ORIGINS=http://localhost:4200


One advantage of doing this is that you can run your local Angular app against your production backend. I’ve found this very useful when debugging and fixing UI issues caused by production data.

Now you should be able to rebuild your Angular Docker container and run it.

Shell
 




xxxxxxxxxx
1


 
1
npm run docker
2
npm run ng-notes


Open your browser to http://localhost:4200, log in, and confirm you can add notes.

Verify the data made it to Heroku by going to https://<your-heroku-app>.herokuapp.com/api/notes.

Deploy Angular + Docker to Heroku

Heroku has several slick features when it comes to Docker images. If your project has a Dockerfile, you can deploy your app directly using the Heroku Container Registry.

First, make sure you’re in the notes directory, then log in to the Container Registry.

Shell
 




xxxxxxxxxx
1


 
1
heroku container:login


Then, create a new app.

Shell
 




xxxxxxxxxx
1


 
1
heroku create


Add the Git URL as a new remote named docker.

Java
 




xxxxxxxxxx
1


 
1
git remote add docker https://git.heroku.com/<your-app-name>.git


You’ll need to update nginx.config so it reads from a $PORT environment variable if it’s set, otherwise default it to 80. You can use envsubst to do this at runtime. However, the default envsubst doesn’t allow default variables. The good news is a8m/envsubst on GitHub does!

Replace your nginx.config with the following configuration that defaults to 80 and escapes the $uri variable so it’s not replaced with a blank value.

notes/nginx.config

Java
 




xxxxxxxxxx
1
11


1
server {
2
    listen       ${PORT:-80};
3
    server_name  _;
4
 
          
5
    root /usr/share/nginx/html;
6
    index index.html;
7
 
          
8
    location / {
9
        try_files $$uri /index.html;
10
    }
11
}


You’ll also need to update your Dockerfile so it uses the aforementioned envsubstr.

notes/Dockerfile

Dockerfile
 




xxxxxxxxxx
1
19


 
1
FROM node:14.1-alpine AS builder
2
 
          
3
WORKDIR /opt/web
4
COPY package.json package-lock.json ./
5
RUN npm install
6
 
          
7
ENV PATH="./node_modules/.bin:$PATH"
8
 
          
9
COPY . ./
10
RUN ng build --prod
11
 
          
12
FROM nginx:1.17-alpine
13
RUN apk --no-cache add curl
14
RUN curl -L https://github.com/a8m/envsubst/releases/download/v1.1.0/envsubst-`uname -s`-`uname -m` -o envsubst && \
15
    chmod +x envsubst && \
16
    mv envsubst /usr/local/bin
17
COPY ./nginx.config /etc/nginx/nginx.template
18
CMD ["/bin/sh", "-c", "envsubst < /etc/nginx/nginx.template > /etc/nginx/conf.d/default.conf && nginx -g 'daemon off;'"]
19
COPY --from=builder /opt/web/dist/notes /usr/share/nginx/html


Then, push your Docker image to Heroku’s Container Registry.

Shell
 




xxxxxxxxxx
1


 
1
heroku container:push web --remote docker


Once the push process has completed, release the image of your app:

Shell
 




xxxxxxxxxx
1


1
heroku container:release web --remote docker


And open the app in your browser:

Shell
 




xxxxxxxxxx
1


1
heroku open --remote docker


You’ll need to add your app’s URL to Okta as a valid redirect URI. In your Spring Boot app on Heroku, go to Resources and click on the Ookta add-on. This will log you in to your Okta dashboard. Navigate to ApplicationsSPAGeneralEdit. Add the following redirect URIs.

  • Login: https://<angular-docker-app>.herokuapp.com/callback

  • Logout: https://<angular-docker-app>.herokuapp.com

You’ll need to add the new app’s URL as an allowed origin in your Spring Boot app on Heroku. Copy the printed Hosting URL value and run the following command.

Shell
 




xxxxxxxxxx
1


 
1
heroku config:edit --remote heroku


Add the new URL after your existing localhost one, separating them with a comma. For example:

Shell
 




xxxxxxxxxx
1


1
ALLOWED_ORIGINS='http://localhost:4200,https://<angular-docker-app>.herokuapp.com'


Now you should be able to log in and see the note you created earlier.

A-Rated Security Headers for Nginx in Docker

If you test your freshly-deployed Angular app with securityheaders.com, you’ll get an F. To solve this, modify your nginx.config to add security headers.

notes/nginx.conf

Java
 




xxxxxxxxxx
1
19


 
1
server {
2
    listen       ${PORT:-80};
3
    server_name  _;
4
 
          
5
    root /usr/share/nginx/html;
6
    index index.html;
7
 
          
8
    location / {
9
        try_files $$uri /index.html;
10
    }
11
 
          
12
    add_header Content-Security-Policy "default-src 'self'; script-src 'self' 'unsafe-eval'; style-src 'self' 'unsafe-inline'; img-src 'self' data:; font-src 'self' data:; frame-ancestors 'none'; connect-src 'self' https://*.okta.com https://*.herokuapp.com";
13
    add_header Referrer-Policy "no-referrer, strict-origin-when-cross-origin";
14
    add_header Strict-Transport-Security "max-age=63072000; includeSubDomains";
15
    add_header X-Content-Type-Options nosniff;
16
    add_header X-Frame-Options DENY;
17
    add_header X-XSS-Protection "1; mode=block";
18
    add_header Feature-Policy "accelerometer 'none'; camera 'none'; microphone 'none'";
19
}


After updating this file, run the following commands:

Shell
 




xxxxxxxxxx
1


 
1
heroku container:push web --remote docker
2
heroku container:release web --remote docker


Now you should get an A!

Commit your changes to Git.

Shell
 




xxxxxxxxxx
1


1
git add .
2
git commit -m "Add Docker for Angular"


Combine Your Angular + Spring Boot App into a Single JAR

In the previous sections, you learned how to deploy your Angular and Spring Boot apps separately. Now I’ll show you how to combine them into a single JAR for production. You’ll still be able to run them independently in development, but deploying them to production will be easier because you won’t have to worry about CORS (cross-origin resource sharing). I’ll also convert the OAuth flows so they all happen server-side, which is more secure as the access token won’t be stored in the browser.


Most client-side OAuth libraries keep access tokens in local storage. However, there is a oauth2-worker project that allows you to store them in a web worker. There’s also folks that think avoiding LocalStorage for tokens is the wrong solution.

Update Your Angular App’s Authentication Mechanism

Create a new AuthService service that will communicate with your Spring Boot API for authentication logic.

notes/src/app/shared/auth.service.ts

TypeScript
 




xxxxxxxxxx
1
52


1
import { Injectable } from '@angular/core';
2
import { Location } from '@angular/common';
3
import { BehaviorSubject, Observable } from 'rxjs';
4
import { HttpClient, HttpHeaders } from '@angular/common/http';
5
import { environment } from '../../environments/environment';
6
import { User } from './user';
7
import { map } from 'rxjs/operators';
8
 
          
9
const headers = new HttpHeaders().set('Accept', 'application/json');
10
 
          
11
@Injectable({
12
  providedIn: 'root'
13
})
14
export class AuthService {
15
  $authenticationState = new BehaviorSubject<boolean>(false);
16
 
          
17
  constructor(private http: HttpClient, private location: Location) {
18
  }
19
 
          
20
  getUser(): Observable<User> {
21
    return this.http.get<User>(`${environment.apiUrl}/user`, {headers}).pipe(
22
      map((response: User) => {
23
        if (response !== null) {
24
          this.$authenticationState.next(true);
25
          return response;
26
        }
27
      })
28
    );
29
  }
30
 
          
31
  isAuthenticated(): Promise<boolean> {
32
    return this.getUser().toPromise().then((user: User) => { 
33
      return user !== undefined;
34
    }).catch(() => {
35
      return false;
36
    })
37
  }
38
 
          
39
  login(): void { 
40
    location.href =
41
      `${location.origin}${this.location.prepareExternalUrl('oauth2/authorization/okta')}`;
42
  }
43
 
          
44
  logout(): void { 
45
    const redirectUri = `${location.origin}${this.location.prepareExternalUrl('/')}`;
46
 
          
47
    this.http.post(`${environment.apiUrl}/api/logout`, {}).subscribe((response: any) => {
48
      location.href = response.logoutUrl + '?id_token_hint=' + response.idToken
49
        + '&post_logout_redirect_uri=' + redirectUri;
50
    });
51
  }
52
}


  1. Talk to the /users endpoint to determine authenticated status. A username will be returned if the user is logged in.
  2. When the user clicks a login button, redirect them to a Spring Security endpoint to do the OAuth dance.
  3. Logout using the /api/logout endpoint, which returns the Okta Logout API URL and a valid ID token.

Create a user.ts file in the same directory, to hold your User model.

notes/src/app/shared/user.ts

TypeScript
 




xxxxxxxxxx
1


1
export class User {
2
  sub: number;
3
  fullName: string;
4
}


Update app.component.ts to use your new AuthService in favor of OktaAuthService.

notes/src/app/app.component.ts

TypeScript
 




xxxxxxxxxx
1
23


1
import { Component, OnInit } from '@angular/core';
2
import { AuthService } from './shared/auth.service';
3
 
          
4
@Component({
5
  selector: 'app-root',
6
  templateUrl: './app.component.html',
7
  styleUrls: ['./app.component.scss']
8
})
9
export class AppComponent implements OnInit {
10
  title = 'Notes';
11
  isAuthenticated: boolean;
12
  isCollapsed = true;
13
 
          
14
  constructor(public auth: AuthService) {
15
  }
16
 
          
17
  async ngOnInit() {
18
    this.isAuthenticated = await this.auth.isAuthenticated();
19
    this.auth.$authenticationState.subscribe(
20
      (isAuthenticated: boolean)  => this.isAuthenticated = isAuthenticated
21
    );
22
  }
23
}


Remove OktaAuthModule and its related code from app.component.spec.ts and home.component.spec.ts. You’ll also need to add HttpClientTestingModule to their TestBed imports.

Change the buttons in app.component.html to reference the auth service instead of oktaAuth.

notes/src/app/app.component.html

HTML
 




xxxxxxxxxx
1


 
1
<button *ngIf="!isAuthenticated" (click)="auth.login()"
2
        class="btn btn-outline-primary" id="login">Login</button>
3
<button *ngIf="isAuthenticated" (click)="auth.logout()"
4
        class="btn btn-outline-secondary" id="logout">Logout</button>


Update home.component.ts to use AuthService too.

notes/src/app/home/home.component.ts

TypeScript
 




xxxxxxxxxx
1
18


 
1
import { Component, OnInit } from '@angular/core';
2
import { AuthService } from '../shared/auth.service';
3
 
          
4
@Component({
5
  selector: 'app-home',
6
  templateUrl: './home.component.html',
7
  styleUrls: ['./home.component.scss']
8
})
9
export class HomeComponent implements OnInit {
10
  isAuthenticated: boolean;
11
 
          
12
  constructor(public auth: AuthService) {
13
  }
14
 
          
15
  async ngOnInit() {
16
    this.isAuthenticated = await this.auth.isAuthenticated();
17
  }
18
}


Delete notes/src/app/auth-routing.module.ts and notes/src/app/shared/okta.

Modify app.module.ts to remove the AuthRoutingModule import, add HomeComponent as a declaration, and import HttpClientModule.

notes/src/app/app.module.ts

TypeScript
 




xxxxxxxxxx
1
26


 
1
import { BrowserModule } from '@angular/platform-browser';
2
import { NgModule } from '@angular/core';
3
 
          
4
import { AppRoutingModule } from './app-routing.module';
5
import { AppComponent } from './app.component';
6
import { NoteModule } from './note/note.module';
7
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
8
import { HomeComponent } from './home/home.component';
9
import { HttpClientModule } from '@angular/common/http';
10
 
          
11
@NgModule({
12
  declarations: [
13
    AppComponent,
14
    HomeComponent
15
  ],
16
  imports: [
17
    BrowserModule,
18
    AppRoutingModule,
19
    HttpClientModule,
20
    NoteModule,
21
    NgbModule
22
  ],
23
  providers: [],
24
  bootstrap: [AppComponent]
25
})
26
export class AppModule { }


Add the route for HomeComponent to app-routing.module.ts.

notes/src/app/app-routing.module.ts

TypeScript
 




xxxxxxxxxx
1


 
1
import { HomeComponent } from './home/home.component';
2
 
          
3
const routes: Routes = [
4
  { path: '', redirectTo: '/home', pathMatch: 'full' },
5
  {
6
    path: 'home',
7
    component: HomeComponent
8
  }
9
];


Change both environments.ts and environments.prod.ts to use a blank apiUrl.

TypeScript
 




xxxxxxxxxx
1


1
apiUrl: ''


Create a proxy.conf.js file to proxy certain requests to your Spring Boot API on http://localhost:8080.

notes/src/proxy.conf.js

JavaScript
 




xxxxxxxxxx
1
10


1
const PROXY_CONFIG = [
2
  {
3
    context: ['/user', '/api', '/oauth2', '/login'],
4
    target: 'http://localhost:8080',
5
    secure: false,
6
    logLevel: 'debug'
7
  }
8
]
9
 
          
10
module.exports = PROXY_CONFIG;


Add this file as a proxyConfig option in angular.json.

notes/angular.json

JSON
 




xxxxxxxxxx
1


 
1
"serve": {
2
  "builder": "@angular-devkit/build-angular:dev-server",
3
  "options": {
4
    "browserTarget": "notes:build",
5
    "proxyConfig": "src/proxy.conf.js"
6
  },
7
  ...
8
},


Remove Okta’s Angular SDK and OktaDev Schematics from your Angular project.

Shell
 




xxxxxxxxxx
1


 
1
npm uninstall @okta/okta-angular @oktadev/schematics


At this point, your Angular app doesn’t contain any Okta-specific code for authentication. Instead, it relies on your Spring Boot app to provide that.

You should still be able to run ng serve in your Angular app and ./gradlew bootRun in your Spring Boot app for local development. However, you’ll need to make some adjustments to your Spring Boot app to include Angular for production.

Configure Spring Boot to Include Your Angular SPA

In your Spring Boot app, you’ll need to change a number of things. You’ll need to configure Gradle to build your Angular app when you pass in -Pprod, you’ll need to adjust its routes (so it’s SPA-aware and routes all 404s to index.html), and you’ll need to modify Spring Security to allow HTML, CSS, and JavaScript to be anonymously accessed.

To begin, delete src/main/kotlin/com/okta/developer/notes/HomeController.kt. You’ll no longer need this because your Angular app will be served up at the / path.

Next, create a RouteController.kt that routes all requests to index.html.

notes-api/src/main/kotlin/com/okta/developer/notes/RouteController.kt

Kotlin
 




xxxxxxxxxx
1
14


 
1
package com.okta.developer.notes
2
 
          
3
import org.springframework.stereotype.Controller
4
import org.springframework.web.bind.annotation.RequestMapping
5
import javax.servlet.http.HttpServletRequest
6
 
          
7
@Controller
8
class RouteController {
9
 
          
10
    @RequestMapping(value = ["/{path:[^\\.]*}"])
11
    fun redirect(request: HttpServletRequest): String {
12
        return "forward:/"
13
    }
14
}


Modify SecurityConfiguration.kt to allow anonymous access to static web files, the /user info endpoint, and to add additional security headers.

notes-api/src/main/kotlin/com/okta/developer/notes/SecurityConfiguration.kt

Kotlin
 




xxxxxxxxxx
1
42


 
1
package com.okta.developer.notes
2
 
          
3
import org.springframework.security.config.annotation.web.builders.HttpSecurity
4
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity
5
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter
6
import org.springframework.security.web.csrf.CookieCsrfTokenRepository
7
import org.springframework.security.web.header.writers.ReferrerPolicyHeaderWriter
8
import org.springframework.security.web.util.matcher.RequestMatcher
9
 
          
10
@EnableWebSecurity
11
class SecurityConfiguration : WebSecurityConfigurerAdapter() {
12
 
          
13
    override fun configure(http: HttpSecurity) {
14
        //@formatter:off
15
        http
16
            .authorizeRequests()
17
                .antMatchers("/**/*.{js,html,css}").permitAll()
18
                .antMatchers("/", "/user").permitAll()
19
                .anyRequest().authenticated()
20
                .and()
21
            .oauth2Login()
22
                .and()
23
            .oauth2ResourceServer().jwt()
24
 
          
25
        http.requiresChannel()
26
                .requestMatchers(RequestMatcher {
27
                    r -> r.getHeader("X-Forwarded-Proto") != null
28
                }).requiresSecure()
29
 
          
30
        http.csrf()
31
                .csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
32
 
          
33
        http.headers()
34
                .contentSecurityPolicy("script-src 'self'; report-to /csp-report-endpoint/")
35
                .and()
36
                .referrerPolicy(ReferrerPolicyHeaderWriter.ReferrerPolicy.SAME_ORIGIN)
37
                .and()
38
                .featurePolicy("accelerometer 'none'; camera 'none'; microphone 'none'")
39
 
          
40
        //@formatter:on
41
    }
42
}


See Spring Security’s headers documentation to see default security headers and other options.

With Kotlin, you can mark parameters and return values as optional by adding ? to their type. Update the user() method in UserController.kt to make OidcUser optional. It will be null when the user is not authenticated, that’s why this change is needed.

notes-api/src/main/kotlin/com/okta/developer/notes/UserController.kt

Kotlin
 




xxxxxxxxxx
1


 
1
@GetMapping("/user")
2
fun user(@AuthenticationPrincipal user: OidcUser?): OidcUser? {
3
    return user;
4
}


Previously, Angular handled logout. Add a LogoutController that will handle expiring the session as well as sending information back to Angular so it can logout from Okta.

notes-api/src/main/kotlin/com/okta/developer/notes/LogoutController.kt

Kotlin
 




xxxxxxxxxx
1
27


1
package com.okta.developer.notes
2
 
          
3
import org.springframework.http.ResponseEntity
4
import org.springframework.security.core.annotation.AuthenticationPrincipal
5
import org.springframework.security.oauth2.client.registration.ClientRegistration
6
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository
7
import org.springframework.security.oauth2.core.oidc.OidcIdToken
8
import org.springframework.web.bind.annotation.PostMapping
9
import org.springframework.web.bind.annotation.RestController
10
import javax.servlet.http.HttpServletRequest
11
 
          
12
@RestController
13
class LogoutController(val clientRegistrationRepository: ClientRegistrationRepository) {
14
 
          
15
    val registration: ClientRegistration = clientRegistrationRepository.findByRegistrationId("okta");
16
 
          
17
    @PostMapping("/api/logout")
18
    fun logout(request: HttpServletRequest,
19
               @AuthenticationPrincipal(expression = "idToken") idToken: OidcIdToken): ResponseEntity<*> {
20
        val logoutUrl = this.registration.providerDetails.configurationMetadata["end_session_endpoint"]
21
        val logoutDetails: MutableMap<String, String> = HashMap()
22
        logoutDetails["logoutUrl"] = logoutUrl.toString()
23
        logoutDetails["idToken"] = idToken.tokenValue
24
        request.session.invalidate()
25
        return ResponseEntity.ok().body<Map<String, String>>(logoutDetails)
26
    }
27
}


In OpenID Connect Logout Options with Spring Boot, Brian Demers describes this as RP-Initiated Logout. He also shows how you can configure Spring Security’s OidcClientInitiatedLogoutSuccessHandler to logout. I tried this technique but decided against it because it doesn’t allow me to redirect back to my Angular app in dev mode. I also encountered some CORS errors that I was unable to solve.

When you access the /user/notes endpoint with Angular, the ${principal.name} expression correctly resolves to the user’s email. However, when you access this endpoint after logging in directly to Spring Boot, it resolves to the sub claim. To make these values consistent, add the following property to application-dev.properties and application-prod.properties.

Properties files
 




xxxxxxxxxx
1


 
1
spring.security.oauth2.client.provider.okta.user-name-attribute=preferred_username


You can also remove the allowed.origins property from both files since Angular will proxy the request in development (eliminating the need for CORS) and there won’t be cross-domain requests in production.

Add a server.port property to application-prod.properties that uses a PORT environment variable, if it’s set.

Properties files
 




xxxxxxxxxx
1


1
server.port=${PORT:8080}


Because there won’t be any cross-domain requests, you can remove the simpleCorsFilter bean and allowedOrigins variable in DemoApplication.kt, too.

Modify Gradle to Build a JAR with Angular Included

Now that your Spring Boot app is ready to serve up your Angular app, you need to modify your Gradle configuration to build your Angular app and package it in the JAR.

Start by importing NpmTask and adding the Node Gradle plugin.

notes/build.gradle.kts

Kotlin
 




xxxxxxxxxx
1


1
import com.moowork.gradle.node.npm.NpmTask
2
 
          
3
plugins {
4
    ...
5
    id("com.github.node-gradle.node") version "2.2.4"
6
    ...
7
}


Then, define the location of your Angular app and configuration for the Node plugin.

Kotlin
 




xxxxxxxxxx
1


 
1
val spa = "${projectDir}/../notes";
2
 
          
3
node {
4
    version = "12.16.2"
5
    nodeModulesDir = file(spa)
6
}


Add a buildWeb task:

Kotlin
 




xxxxxxxxxx
1


 
1
val buildWeb = tasks.register<NpmTask>("buildNpm") {
2
    dependsOn(tasks.npmInstall)
3
    setNpmCommand("run", "build")
4
    setArgs(listOf("--", "--prod"))
5
    inputs.dir("${spa}/src")
6
    inputs.dir(fileTree("${spa}/node_modules").exclude("${spa}/.cache"))
7
    outputs.dir("${spa}/dist")
8
}


And modify the processResources task to build Angular when -Pprod is passed in.

Kotlin
 




xxxxxxxxxx
1


 
1
tasks.processResources {
2
    rename("application-${profile}.properties", "application.properties")
3
    if (profile == "prod") {
4
        dependsOn(buildWeb)
5
        from("${spa}/dist/notes") {
6
            into("static")
7
        }
8
    }
9
}


Now you should be able to combine both apps using ./gradlew bootJar -Pprod. Once it’s built, run it with the following commands to ensure everything works.

Shell
 




xxxxxxxxxx
1


 
1
docker-compose -f src/main/docker/postgresql.yml up -d
2
source okta.env
3
java -jar build/libs/*.jar


Congrats! You modified your Angular and Spring Boot apps to be packaged together and implemented the most secure form of OAuth 2.0 to boot! ��

Commit your changes to Git.

Shell
 




xxxxxxxxxx
1


 
1
git add .
2
git commit -m "Combine Angular and Spring Boot"


Dockerize Angular + Spring Boot with Jib

Since everything is done via Gradle now, you can use plugins to build a Docker container. Jib builds optimized Docker images without the need for deep mastery of Docker best-practices. It reads your Gradle/Maven build files for its metadata.

To add Jib support, add its Gradle plugin.

notes/build.gradle.kts

Shell
 




xxxxxxxxxx
1


 
1
plugins {
2
    ...
3
    id("com.google.cloud.tools.jib") version "2.4.0"
4
}


Then, at the end of this file, add jib configuration to specify your image name and the active Spring profile.

Java
 




xxxxxxxxxx
1


1
jib {
2
    to {
3
        image = "<your-username>/bootiful-angular"
4
    }
5
    container {
6
        environment = mapOf("SPRING_PROFILES_ACTIVE" to profile)
7
    }
8
}


Run the following command to build a Docker image with Jib.

Shell
 




xxxxxxxxxx
1


 
1
./gradlew jibDockerBuild -Pprod


If you want to override the image name in build.gradle.kts, you can pass in an --image parameter. For example, ./gradlew jibDockerBuild -Pprod --image=bootiful-ng9.

Run Your Spring Boot Docker App with Docker Compose

In theory, you should be able to run the following command to run your app.

Shell
 




xxxxxxxxxx
1


 
1
docker run --publish=8080:8080 <your-username>/bootiful-angular


However, Spring Boot won’t start because you haven’t configured the Okta environment variables. You could pass them in on the command line, but it’s easier to specify them in a file.

You can use Docker Compose and its env_file option to specify environment variables.

Copy notes-api/okta.env to src/main/docker/.env.

Remove export at the beginning of each line. It should resemble something like the following after this change:

Shell
 




xxxxxxxxxx
1


1
OKTA_OAUTH2_ISSUER=https://dev-210914.okta.com/oauth2/default
2
OKTA_OAUTH2_CLIENT_ID=0oaa7psy...
3
OKTA_OAUTH2_CLIENT_SECRET=FJcSFpTC6N...


Create a src/main/docker/app.yml file that configures your app to set environment variables and leverages your existing PostgreSQL container. Make sure to replace the <your-username> placeholder and make the image match what’s in your build.gradle.kts file.

YAML
 




xxxxxxxxxx
1
17


 
1
version: '2'
2
services:
3
  boot-app:
4
    image: <your-username>/bootiful-angular
5
    environment:
6
      - SPRING_DATASOURCE_URL=jdbc:postgresql://notes-postgresql:5432/notes
7
      - OKTA_OAUTH2_ISSUER=${OKTA_OAUTH2_ISSUER}
8
      - OKTA_OAUTH2_CLIENT_ID=${OKTA_OAUTH2_CLIENT_ID}
9
      - OKTA_OAUTH2_CLIENT_SECRET=${OKTA_OAUTH2_CLIENT_SECRET}
10
    ports:
11
      - 8080:8080
12
    depends_on:
13
      - notes-postgresql
14
  notes-postgresql:
15
    extends:
16
      file: postgresql.yml
17
      service: notes-postgresql


Docker Compose expects the .env file to be in the directory you run docker-compose from, so you have two choices:

  1. Navigate to the src/main/docker directory before running docker-compose
  2. Create a symlink to .env in your root directory: ln -s src/main/docker/.env

If you choose option #1, run:

Shell
 




xxxxxxxxxx
1


 
1
cd src/main/docker
2
docker-compose -f app.yml up


Option #2 looks like:

Shell
 




xxxxxxxxxx
1


1
docker-compose -f src/main/docker/app.yml up


Once you’ve verified everything works, commit your changes to Git.

Shell
 




xxxxxxxxxx
1


 
1
git add .
2
git commit -m "Add Jib to build Docker images"


Deploy Your Spring Boot + Angular Container to Docker Hub

Jib makes it incredibly easy to deploy your container to Docker Hub. If you don’t already have a Docker Hub account, you can create one.

Run docker login to log into your account, then use the jib task to build and deploy your image.

Shell
 




xxxxxxxxxx
1


 
1
./gradlew jib -Pprod


Isn’t it cool how Jib makes it so you don’t need a Dockerfile!? ��

Heroku �� Spring Boot + Docker

To deploy this container to Heroku, create a new Heroku app and add it as a Git remote.

Shell
 




xxxxxxxxxx
1


 
1
heroku create
2
git remote add jib https://git.heroku.com/<your-new-app>.git


At this point, you can use the PostgreSQL and Okta add-ons you’ve already configured. If you’d like to do this, use addons:attach instead of addons:create in the following commands. Since both add-ons are free, I’m just going to show how to create new ones.

Add PostgreSQL to this app and configure it for Spring Boot using the following commands:

Shell
 




xxxxxxxxxx
1


 
1
heroku addons:create heroku-postgresql --remote jib
2
heroku config:get DATABASE_URL --remote jib
3
heroku config:set SPRING_DATASOURCE_URL=jdbc:postgresql://<value-after-@-from-last-command> --remote jib
4
heroku config:set SPRING_DATASOURCE_USERNAME=<username-value-from-last-command> --remote jib
5
heroku config:set SPRING_DATASOURCE_PASSWORD=<password-value-from-last-command> --remote jib
6
heroku config:set SPRING_DATASOURCE_DRIVER_CLASS_NAME=org.postgresql.Driver --remote jib


This fine-grained configuration is not necessary when you use Heroku’s buildpacks to deploy your Spring Boot app. It injects scripts that set SPRING_* environment variables for you. In this case, Heroku doesn’t know you’re using Spring Boot since it’s running in a container.

Add Okta to your app.

Shell
 




xxxxxxxxxx
1


1
heroku addons:create okta --remote jib


To see your database and Okta environment variables, run:

Shell
 




xxxxxxxxxx
1


 
1
heroku config --remote jib


Modify the Okta environment variables to remove the _WEB on the two keys that have it.

Shell
 




xxxxxxxxxx
1


 
1
heroku config:edit --remote jib


Run the commands below to deploy the image you deployed to Docker Hub. Be sure to replace the <…> placeholders with your username and app name.

Shell
 




xxxxxxxxxx
1


 
1
docker tag <your-username>/bootiful-angular registry.heroku.com/<heroku-app>/web
2
docker push registry.heroku.com/<heroku-app>/web
3
heroku container:release web --remote jib


For example, I used:

Shell
 




xxxxxxxxxx
1


1
docker tag mraible/bootiful-angular registry.heroku.com/enigmatic-woodland-19325/web
2
docker push registry.heroku.com/enigmatic-woodland-19325/web
3
heroku container:release web --remote jib


You can watch the logs to see if your container started successfully.

Shell
 




xxxxxxxxxx
1


 
1
heroku logs --tail --remote jib


Once you’ve verified it has started OK, set the Hibernate configuration so it only validates the schema.

Shell
 




xxxxxxxxxx
1


 
1
heroku config:set SPRING_JPA_HIBERNATE_DDL_AUTO=validate --remote jib


Since the Okta Add-on for Heroku configures everything for you, you should be able to open your app, click the Login button, and authenticate!

Shell
 




xxxxxxxxxx
1


 
1
heroku open --remote jib


If you test your Dockerfied Angular + Spring Boot app on securityheaders.com, you’ll see it scores an A+!

Knative �� Spring Boot + Docker

Heroku is awesome, but sometimes people want more control over their infrastructure. Enter Knative. It’s like Heroku in that it’s a Platform as a Service (PaaS). Knative is built on top of Kubernetes so you can install a number of services with a bit of YAML and kubectl commands.

With Heroku, when companies reach the limitations of the platform, they have to go elsewhere to host their services. With Knative, you can just drop down to Kubernetes. It’s Heroku for Kubernetes in a sense, but you don’t have to switch to a different universe when you need additional functionality.

The Knative website says it’ll make your developers more productive.

Knative components build on top of Kubernetes, abstracting away the complex details and enabling developers to focus on what matters. Built by codifying the best practices shared by successful real-world implementations, Knative solves the "boring but difficult" parts of deploying and managing cloud native services, so you don’t have to.

You’ll need a Google Cloud account for this section. Go to cloud.google.com and click Get started for free.

Once you have an account, go to Google Cloud Console and create a new project.

Then, click on the Terminal icon in the top right to open a Cloud Shell terminal for your project.

Enable Cloud and Container APIs:

Shell
 




xxxxxxxxxx
1


 
1
gcloud services enable \
2
  cloudapis.googleapis.com \
3
  container.googleapis.com \
4
  containerregistry.googleapis.com


This command can take a minute or two to complete.

When it’s done, set your default zone and region:

Shell
 




xxxxxxxxxx
1


1
gcloud config set compute/zone us-central1-c
2
gcloud config set compute/region us-central1


And create a Kubernetes cluster:

Shell
 




xxxxxxxxxx
1


 
1
gcloud beta container clusters create knative \
2
  --addons=HorizontalPodAutoscaling,HttpLoadBalancing \
3
  --machine-type=n1-standard-4 \
4
  --cluster-version=1.15 \
5
  --enable-stackdriver-kubernetes --enable-ip-alias \
6
  --enable-autoscaling --min-nodes=5 --num-nodes=5 --max-nodes=10 \
7
  --enable-autorepair \
8
  --scopes cloud-platform


You can safely ignore the warnings that result from running this command.

Next, set up a cluster administrator and install Istio.

Shell
 




xxxxxxxxxx
1
13


 
1
kubectl create clusterrolebinding cluster-admin-binding \
2
  --clusterrole=cluster-admin \
3
  --user=$(gcloud config get-value core/account)
4
 
          
5
kubectl apply -f \
6
https://github.com/knative/serving/raw/v0.14.0/third_party/istio-1.5.1/istio-crds.yaml
7
 
          
8
while [[ $(kubectl get crd gateways.networking.istio.io -o jsonpath='{.status.conditions[?(@.type=="Established")].status}') != 'True' ]]; do
9
  echo "Waiting on Istio CRDs"; sleep 1
10
done
11
 
          
12
kubectl apply -f \
13
https://github.com/knative/serving/raw/v0.14.0/third_party/istio-1.5.1/istio-minimal.yaml


Now, you should be able to install Knative!

Shell
 




xxxxxxxxxx
1
10


1
kubectl apply --selector knative.dev/crd-install=true -f \
2
 https://github.com/knative/serving/releases/download/v0.14.0/serving.yaml
3
 
          
4
kubectl apply -f \
5
 https://github.com/knative/serving/releases/download/v0.14.0/serving.yaml
6
 
          
7
while [[ $(kubectl get svc istio-ingressgateway -n istio-system \
8
  -o 'jsonpath={.status.loadBalancer.ingress[0].ip}') == '' ]]; do
9
  echo "Waiting on external IP"; sleep 1
10
done


You’ll need a domain to enable HTTPS, so set that up and point it to the cluster’s IP address.

Shell
 




xxxxxxxxxx
1
13


 
1
export IP_ADDRESS=$(kubectl get svc istio-ingressgateway -n istio-system \
2
  -o 'jsonpath={.status.loadBalancer.ingress[0].ip}')
3
echo $IP_ADDRESS
4
 
          
5
kubectl apply -f - <<EOF
6
apiVersion: v1
7
kind: ConfigMap
8
metadata:
9
  name: config-domain
10
  namespace: knative-serving
11
data:
12
  $IP_ADDRESS.nip.io: ""
13
EOF


Install cert-manager to automatically provision and manage TLS certificates in Kubernetes.

Shell
 




xxxxxxxxxx
1


1
kubectl apply --validate=false -f \
2
 https://github.com/jetstack/cert-manager/releases/download/v0.14.3/cert-manager.yaml
3
 
          
4
kubectl wait --for=condition=Available -n cert-manager deployments/cert-manager-webhook


And configure free TLS certificate issuing with Let’s Encrypt.

Shell
 




xxxxxxxxxx
1
41


 
1
kubectl apply -f - <<EOF
2
apiVersion: cert-manager.io/v1alpha2
3
kind: ClusterIssuer
4
metadata:
5
  name: letsencrypt-http01-issuer
6
spec:
7
  acme:
8
    privateKeySecretRef:
9
      name: letsencrypt
10
    server: https://acme-v02.api.letsencrypt.org/directory
11
    solvers:
12
    - http01:
13
       ingress:
14
         class: istio
15
EOF
16
 
          
17
kubectl apply -f \
18
https://github.com/knative/serving/releases/download/v0.14.0/serving-cert-manager.yaml
19
 
          
20
kubectl apply -f - <<EOF
21
apiVersion: v1
22
kind: ConfigMap
23
metadata:
24
  name: config-certmanager
25
  namespace: knative-serving
26
data:
27
  issuerRef: |
28
    kind: ClusterIssuer
29
    name: letsencrypt-http01-issuer
30
EOF
31
 
          
32
kubectl apply -f - <<EOF
33
apiVersion: v1
34
kind: ConfigMap
35
metadata:
36
  name: config-network
37
  namespace: knative-serving
38
data:
39
  autoTLS: Enabled
40
  httpProtocol: Enabled
41
EOF


Phew! That was a lot of kubectl and YAML, don’t you think?! The good news is you’re ready to deploy PostgreSQL and your Spring Boot app.

The following command can deploy everything, but you’ll need to change the <…> placeholders to match your values first.

Shell
 




xxxxxxxxxx
1
80


 
1
kubectl apply -f - <<EOF
2
apiVersion: v1
3
kind: PersistentVolumeClaim
4
metadata:
5
  name: pgdata
6
  annotations:
7
    volume.alpha.kubernetes.io/storage-class: default
8
spec:
9
  accessModes: [ReadWriteOnce]
10
  resources:
11
    requests:
12
      storage: 1Gi
13
---
14
apiVersion: apps/v1beta1
15
kind: Deployment
16
metadata:
17
  name: postgres
18
spec:
19
  replicas: 1
20
  template:
21
    metadata:
22
      labels:
23
        service: postgres
24
    spec:
25
      containers:
26
        - name: postgres
27
          image: postgres:10.1
28
          ports:
29
            - containerPort: 5432
30
          env:
31
            - name: POSTGRES_DB
32
              value: bootiful-angular
33
            - name: POSTGRES_USER
34
              value: bootiful-angular
35
            - name: POSTGRES_PASSWORD
36
              value: <your-db-password>
37
          volumeMounts:
38
            - mountPath: /var/lib/postgresql/data
39
              name: pgdata
40
              subPath: data
41
      volumes:
42
        - name: pgdata
43
          persistentVolumeClaim:
44
            claimName: pgdata
45
---
46
apiVersion: v1
47
kind: Service
48
metadata:
49
  name: pgservice
50
spec:
51
  ports:
52
  - port: 5432
53
    name: pgservice
54
  clusterIP: None
55
  selector:
56
    service: postgres
57
---
58
apiVersion: serving.knative.dev/v1alpha1
59
kind: Service
60
metadata:
61
  name: bootiful-angular
62
spec:
63
  template:
64
    spec:
65
      containers:
66
        - image: <your-username>/bootiful-angular
67
          env:
68
          - name: SPRING_DATASOURCE_URL
69
            value: jdbc:postgresql://pgservice:5432/bootiful-angular
70
          - name: SPRING_DATASOURCE_USERNAME
71
            value: bootiful-angular
72
          - name: SPRING_DATASOURCE_PASSWORD
73
            value: <your-db-password>
74
          - name: OKTA_OAUTH2_ISSUER
75
            value: <your-okta-issuer>
76
          - name: OKTA_OAUTH2_CLIENT_ID
77
            value: <your-okta-client-id>
78
          - name: OKTA_OAUTH2_CLIENT_SECRET
79
            value: <your-okta-client-secret>
80
EOF


Once the deployment has completed, run the command below to change it so Hibernate doesn’t try to recreate your schema on restart.

Shell
 




xxxxxxxxxx
1
26


 
1
kubectl apply -f - <<EOF
2
apiVersion: serving.knative.dev/v1alpha1
3
kind: Service
4
metadata:
5
  name: bootiful-angular
6
spec:
7
  template:
8
    spec:
9
      containers:
10
        - image: <your-username>/bootiful-angular
11
          env:
12
          - name: SPRING_DATASOURCE_URL
13
            value: jdbc:postgresql://pgservice:5432/bootiful-angular
14
          - name: SPRING_DATASOURCE_USERNAME
15
            value: bootiful-angular
16
          - name: SPRING_DATASOURCE_PASSWORD
17
            value: <your-db-password>
18
          - name: OKTA_OAUTH2_ISSUER
19
            value: <your-okta-issuer>
20
          - name: OKTA_OAUTH2_CLIENT_ID
21
            value: <your-okta-client-id>
22
          - name: OKTA_OAUTH2_CLIENT_SECRET
23
            value: <your-okta-client-secret>
24
          - name: SPRING_JPA_HIBERNATE_DDL_AUTO
25
            value: validate
26
EOF


If everything works correctly, you should be able to run the following command to get the URL of your app.

Shell
 




xxxxxxxxxx
1


 
1
kubectl get ksvc bootiful-angular


The result should look similar to this:

Shell
 




xxxxxxxxxx
1


 
1
NAME               URL                                                    LATESTCREATED            LATESTREADY              READY   REASON
2
bootiful-angular   https://bootiful-angular.default.34.70.42.153.nip.io   bootiful-angular-qf9hn   bootiful-angular-qf9hn   True


You’ll need to add this URL (+ /login/oauth2/code/okta) as a Login redirect URI and a Logout redirect URI in Okta in order to log in.

Then, you’ll be able to log into your app running on Knative! Add a note or two to prove it all works.

Cloud Foundry �� Spring Boot + Docker

Did you know you can run Docker containers on Cloud Foundry? It’s pretty slick.

If you’d like to test it out, you’ll need a Pivotal Web Services account. You’ll also need to install the Cloud Foundry CLI. If you’re using Homebrew, you can use brew install cloudfoundry/tap/cf-cli.

Apps deployed to Cloud Foundry (CF) with the cf push command run in standard CF Linux containers. With Docker support enabled, CF can also deploy and manage apps running in Docker containers.

Then, where secure-notes is a unique name for your app, run the following commands.

Shell
 




xxxxxxxxxx
1
10


 
1
cf login
2
 
          
3
# Deploy the image from Docker Hub
4
cf push --no-start -o <your-username>/bootiful-angular secure-notes
5
 
          
6
# Create a PostgreSQL instance
7
cf cs elephantsql turtle secure-notes-psql
8
 
          
9
# Bind the app to the PostgreSQL instance
10
cf bs secure-notes secure-notes-psql


At this point, you’ll need to set a number of environment variables so your app can connect to PostgreSQL and Okta. Substitute your values in the <…> placeholders before running the command below.

To get your PostgreSQL URL run the following command where secure-notes is your app name.

Shell
 




xxxxxxxxxx
1


 
1
cf env secure-notes


You will see a uri at the top of the output that has the URL you’ll need to parse and set as variables below. Make sure to replace postgres:// with jdbc:postgresql:// at the beginning of the datasource URL and extract the credentials for the username and password settings.

Shell
 




xxxxxxxxxx
1


 
1
export APP_NAME=<your-app-name>
2
cf set-env $APP_NAME SPRING_DATASOURCE_DRIVER_CLASS_NAME org.postgresql.Driver
3
cf set-env $APP_NAME SPRING_DATASOURCE_URL <postgresql-jdbc-url>
4
cf set-env $APP_NAME SPRING_DATASOURCE_USERNAME <postgresql-username>
5
cf set-env $APP_NAME SPRING_DATASOURCE_PASSWORD <postgresql-passord>
6
cf set-env $APP_NAME OKTA_OAUTH2_ISSUER <your-okta-issuer>
7
cf set-env $APP_NAME OKTA_OAUTH2_CLIENT_ID <your-okta-client-id>
8
cf set-env $APP_NAME OKTA_OAUTH2_CLIENT_SECRET <your-okta-client-id>
9
cf restage $APP_NAME


Your app, running in Docker, should now be available at http://<your-app-name>.cfapps.io.

You’ll need to add this URL (+ /login/oauth2/code/okta) as a Login redirect URI and Logout redirect URI on Okta in order to log in.

You’ll also want to configure JPA so it doesn’t recreate the schema on each restart.

Shell
 




xxxxxxxxxx
1


 
1
cf set-env $APP_NAME SPRING_JPA_HIBERNATE_DDL_AUTO validate


Now you should be able to loginlog in and add notes to your heart’s content!

You can also just use a manifest.yml to make it so you don’t have to type all the commands above.

YAML
 




xxxxxxxxxx
1
20


1
applications:
2
- name: <your-app-name>
3
  disk_quota: 1G
4
  docker:
5
    image: <your-username>/bootiful-angular
6
  env:
7
    OKTA_OAUTH2_CLIENT_ID: <your-okta-client-id>
8
    OKTA_OAUTH2_CLIENT_SECRET: <your-okta-client-secret>
9
    OKTA_OAUTH2_ISSUER: <your-okta-issuer>
10
    SPRING_DATASOURCE_DRIVER_CLASS_NAME: org.postgresql.Driver
11
    SPRING_DATASOURCE_PASSWORD: <postgresql-password>
12
    SPRING_DATASOURCE_URL: <postgresql-jdbc-url>
13
    SPRING_DATASOURCE_USERNAME: <postgresql-username>
14
instances: 1
15
  memory: 1G
16
  routes:
17
    - route: http://<your-app-name>.cfapps.io
18
  services:
19
    - <your-app-name>-psql
20
  stack: cflinuxfs3


Since most of these environment variables should probably be externally specified, it’s not much gain to use the manifest.yml in this case. Storing secrets in source control is a bad idea!


If you do decide to store everything in manifest.yml, make sure to add it to .gitignore.

With a manifest.yml in place, you can simply run cf push and it’ll do the same thing as the aforementioned cf commands.

Use Cloud Native Buildpacks to Build Docker Images

Cloud Native Buildpacks is an initiative that was started by Pivotal and Heroku in early 2018. It has a pack CLI that allows you to build Docker images using buildpacks.

Unfortunately, pack doesn’t have great support for monorepos (especially in sub-directories) yet. I was unable to make it work with this app structure.

On the upside, Spring Boot 2.3’s built-in support for creating Docker images works splendidly!

Easy Docker Images with Spring Boot 2.3

Spring Boot 2.3.0 is now available and with it comes built-in Docker support. It leverages Cloud Native Buildpacks, just like the pack CLI.

Spring Boot’s Maven and Gradle plugins both have new commands:

  • ./mvnw spring-boot:build-image

  • ./gradlew bootBuildImage

The Paketo Java buildpack is used by default to create images.


Learn more in Phil Webb’s blog post: Creating Docker images with Spring Boot 2.3.0.M1 or his excellent What’s new in Spring Boot 2.3 video.

By default, Spring Boot will use your $artifactId:$version for the image name. That is, notes-api:0.0.1-SNAPSHOT. You can override this with an --imageName parameter.

Build and run the image with the commands below.

Shell
 




xxxxxxxxxx
1


 
1
./gradlew bootBuildImage --imageName <your-username>/bootiful-angular -Pprod
2
docker-compose -f src/main/docker/app.yml up


You should be able to navigate to http://localhost:8080, log in, and add notes.

Pretty neat, don’t you think!? ��

Learn More About Angular, Spring Boot, and Docker

This lengthy tutorial showed you a lot of options when it comes to deploying your Angular and Spring Boot apps with Docker:

  • Build Angular containers with Dockerfile

  • Combine Angular and Spring Boot in a JAR

  • Build Docker images with Jib

  • Build Docker images with Buildpacks

You can find the source code for this example on GitHub at oktadeveloper/okta-angular-spring-boot-docker-example.

As a developer, you probably don’t want to do a series of tutorials to get to a baseline to start a project. The good news is JHipster does everything in this series. It allows you to run your Angular and Spring Boot apps separately, use Kotlin on the server, package your apps together for production, and use Docker for distribution.

To learn more about Angular, Spring Boot, and Docker, see some of our other blog posts.

Follow @oktadev on Twitter for more posts like this one. We also have a YouTube channel you might enjoy. As always, please leave a comment below if you have any questions!

Topics:
angular, bootstrap, cloud foundry, docker, google cloud, heroku, knative, spring boot

Published at DZone with permission of Matt Raible , DZone MVB. See the original article here.

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}