Contents

Dockerize Spring Boot App with Frontend in React

Frontend for stoic application

Some time ago I created simple Spring Boot app with basic RESTful API and PostgreSQL backend (see this post in which I document my struggle with my first Docker app). Now is the time to create a simple frontend.

A very basic backend application stoic_cafe requires a simple GUI so that I can hit my RESTful endpoints through the browser. Let’s write a simple javascript client that will do just that:

  • display a list of quotes and a list of thoughts
  • allow edition and deletion

I initially decided to start with Preact which somehow seems to be a bit less scary than React.

Set up preact application

I started with a template application as shown in preact.js guide:

1
npx preact-cli create default my-project

Then I started the app in development mode and started to play.

1
npm run dev

Components

I modified autogenerated Home component a bit. I wanted to display Quotes component (that would - when mounted - display a list of Quote components).

Quote

The Quote component’s code is really simple. Here I assume that I would receive simple properties for display (quote, author) and delete callback which I call when button is clicked.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
function Quote(props) {
  var deleteWithId = (e)=>props.delete(props.id);
  return  (
  <div class = {style.thought} id="thought">
    <p>{props.quote}</p>
    <h3>{props.author}</h3>
    <button class={style.button} onClick={deleteWithId}>delete</button>
  </div>
  );
}

Quotes

The callback is received as a property from parent component (Quotes) that manages all quotes as its state. Here is hoq Quotes is implemented:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
class Quotes extends Component {
  constructor(props) {
    super(props);
    this.state = { thoughts: []}
  }

  getQuoteList() {
    fetch(urls.getQuotesUrl,
      {method:"GET",
      headers:{"Content-Type":"application/json;encoding=UTF-8;"}})
    .then(res => res.json())
    .then(json => {
      this.setState({thoughts: json});
    });
  }

  componentDidMount() {
    this.getQuoteList();
  }

  deleteById(id){
    fetch(urls.deleteQuoteUrl(id), {method:'delete'})
    .catch(
      (err) => console.log("error", err)
    ).finally (
      (resp) =>{
        console.log("this is", this)
        this.getQuoteList()
      }
    );
  }

  render(props, state) {
    return (
      <div class={style.thoughts}>
        {state.thoughts.length > 0 && (
        <ul >
        {state.thoughts.map(t =>
            <li>
              <Quote
              key={t.id}
              author={t.author}
              quote={t.text}
              id={t.id}
              delete={(id) => this.deleteById(id)}/>
            </li>
          )}
        </ul>
        )}
      </div>)
  }
}
export default Quotes;

Fetch api

In order to fetch data from server and also to send other types of requests (POST new quotes or DELETE them), I use fetch API.

CORS

In order to be able to access responses from server I needed to use CORS in a very simple way:

  • all requests - from all origins - are allowed
  • methods allowed are: GET, POST and DELETE

After a while I changed my assumptions a bit:

  • only allow connections from localhost
  • and only from two ports:
    • server application port for pure REST
    • and also connections from gui application (also runnig on localhost)

Configuration in Spring Boot

In Spring Boot this is very simple. I just need to configure one @Bean which uses CorsRegistration instances for specific cors rules.

  • I don’t have any separation of cors rules for different parts of my app, therefore the mapping I define is for all paths (addMapping("/**"))
  • I want my app to be accessible from the browser, therefore I allow access from server.port
  • I also want to access the app from the Preact client application, which I want to run on gui.port
  • All access is done from localhost, hence the localhost host is hardcoded
 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
    @Bean
    public WebMvcConfigurer corsConfigurer(Environment env) {
        final int guiPort = env.getProperty("gui.port", Integer.class, 8081);
        final int serverPort = env.getProperty("server.port", Integer.class, 5000);
        final String host = "http://localhost:%s";


        return new WebMvcConfigurer() {
            @Override
            public void addCorsMappings(CorsRegistry registry) {
                registry.addMapping("/**")
                    .allowedOrigins(host.formatted(guiPort), host.formatted(serverPort))
                    .allowedMethods("GET", "POST", "DELETE");
            }
        };
    }

Note to self: have a look at Spring Cloud Config

React, not Preact

I wanted to experiment a bit with react component libraries and Preact with its compaibility layer was a bit inconvenient. So I used create-react-app.

React App issues

I had a lot of unknowns with my little app:

  • in Dockerfile, how to pass environment variables to the application? I need to pass server.port (for RESTful API) and gui.port (to allow the client in COSR) …
  • what happens if I npm run build instead of npm run?
  • how to write ENTRYPOINT ?

I just tried to follow this great tutorial which came first when I searched for dockerize create-react-app.

But I’ll be honest, I don’t know what I’m doing.

My Dockerfile for React app

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
dockerize create-react-app

FROM node:19 as build 

ENV NODE_ENV production

WORKDIR /react-app

COPY package.json .
COPY package-lock.json .

RUN npm install

COPY . .
ARG server_port 5000
ARG gui_port 8000
ENV REACT_APP_SERVER_URL=http://localhost:${server_port} PORT=${gui_port}

RUN npm run build

FROM nginx:1.19

ENV NODE_ENV production
COPY --from=build /react-app/build /usr/share/nginx/html

My docker-compose

And finally my docker-compose:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
version: '2'

services:
  app:
    image: 'stoic-backend:v1.0.0'

    build:
      context: .
    container_name: boot-stoic-docker-app
    depends_on:
      - db
    environment:
      - SPRING_DATASOURCE_URL=jdbc:postgresql://db:5432/stoic_db
      - SPRING_DATASOURCE_USERNAME=stoic
      - SPRING_DATASOURCE_PASSWORD=stoic
      - SERVER_PORT=${SERVER_PORT}
      - GUI_PORT=${GUI_PORT}
    ports:
      - "8081:${SERVER_PORT}"

  gui:
    image: 'kamila/react_simple_dock:latest'
    container_name: react_simple_dock
    build:
      context: ../simple
    depends_on:
      - app
    environment:
      SERVER_PORT: ${SERVER_PORT}
      GUI_PORT: ${GUI_PORT}
    ports:
      - "8000:${GUI_PORT}"
  db:
    image: 'postgres:13.1-alpine'
    container_name: db
    environment:
      - POSTGRES_USER=stoic
      - POSTGRES_PASSWORD=stoic
      - POSTGRES_DB=stoic_db
    volumes:
      - ./postgres-data:/var/lib/postgresql/data

As you can see, I use two environment variables, GUI_PORT and SERVER_PORT and pass them:

  • to app service so that the app can correctly configure cors for connections from server and from client
  • to gui service - it actually only needs SERVER_PORT to know what port to hit when accessing app

Random problems

During development of a simple gui client I stumbled upon several small “problems”. The solutions to those problems are certainly very well known for full stack devs, but for me they were just little obstacles that made my way to learn more. Here is what I have lerned:

SequenceGenerator

Postresql default sequence allocation size is 1 and Hibernate (or JPA) by default uses value 50. I’ve noticed that an exception is raised when Hibernate tries to create the genrator on its side (see problem and its allocation size differs from the allocation size I defined in database sequence.

The solution and a good practice is this:

  • be explicit when declaring allocation size in @SequenceGenerator:
    1
    2
    3
    4
    
        @Id
        @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = GENERATOR_NAME)
        @SequenceGenerator(name = GENERATOR_NAME, sequenceName = SEQUENCE_NAME, allocationSize = 50)
        private Long id;
    
  • use the same value in sql code and in JPA code; here is what I needed to do:
    1
    2
    3
    
    create sequence hibernate_sequence start 1 increment 1;
    -- (... and in separate changeset...)
    alter sequence hibernate_sequence increment by 50;
    

Who stole my port

I sometimes forgot that I started my app in the background and I’m supprised to see that the port I want to start another one is busy. In order to identify the PID of the java process I can use a very useful tool - lsof.

1
2
3
[karma@tpd|~] sudo lsof -i:5000
COMMAND    PID  USER   FD   TYPE DEVICE SIZE/OFF NODE NAME
java    185048 karma   12u  IPv6 330359      0t0  TCP *:5000 (LISTEN)

Stopping a spring-boot app

Once the PID is found, I can simply

1
kill -15 185048

which is a kind notification that the app should shut down (instead of kill -9 <PID> which causes the OS to forcibly kill the app)

Of course, a more system-friendly way of sutting down the app, when it was started with

1
mvn spring-boot:start

is to use:

1
mvn spring-boot:stop

This ensures proper closing of all resources, like connections to the database. When spring-boot app is run with

1
mvn sprint-boot:run

then it stays connected to the terminal and simple CTRL+C is enough to stop it gracefully.

See this article for a description of how to close spring-boot application.

Spring Boot testing

This is a very wide topic. How to structure and how to define tests?

  • unit tests don’t depend on spring boot classes
  • integration tests do; they either:
    • test servlet layer (test annotated @WebMvcTest with autowired MockMvc) These tests hit the APIs, pass the path parameters usingMockMvcRequestBuilders and verify the status response codes and response content using MockMvcResultMatchers and MockMvcResultHandlers.
    • data tests (question: how to properly test? how to setup the database? how to use testcontainers?)

See this spring boot testing miniseries by Arho Huttunen and his blog for in-depth explanation of different types of testing a spring-boot application.

Building Uris

Don’t forget to properly build uris (e.g. when constructing Location header):

1
2
final String baseUrl = 
ServletUriComponentsBuilder.fromCurrentContextPath().build().toUriString();

Expected exception

Reminder on how to test application exceptions in JUnit5:

1
2
3
4
5
6
7
8
9
@Test
void testExpectedException() {

  ApplicationException thrown = Assertions.assertThrows(ApplicationException.class, () -> {
           //Code under test
  });

  Assertions.assertEquals("some message", exception.getMessage());
}

Hardcoding

My current solution can be improved by:

  • writing docker-compose.yml such that it also uses environment variables and not hardcoded values for port mapping
  • use the outside port in my (p)react client code that connects to that outside-facing service
  • define an environment variable which says on which port my (p)react client is accessible as an external service (and perhaps then a port mapping for spring-boot app is not needed at all? - well, it would be nice to have e.g. monitoring entry point)

And it would be nice if I could use a dockerized DNS server so that I can have nice domain names. I wonder if this is doable. Let’s check it out…

Why useEffect is running twice?

This answer: useeffect-is-running-twice-on-mount-in-react explains a bit and moves on right direction, which is: go learn from the official documentation about effects

How to run POST from commandline

I’m always struggling with formatting json in bash for curl consumption so I immediatelly instaled http which comes from httpie.

Thanks to http I can run

1
http localhost:5000/quote/  author=kamila text="It is hard to be a stoic with a toddler. And I am built to handle hardness. Therefore I will go change this diaper and will think about Marcus Aurelius."

Why npm build script hits 8080

I’m not sure why it happens but the Reeac app built using npm build and started by exposing ./build in, let’s say, python3’s http.server , would nicely use environment variable passed to python process (perhaps this is not what happens).

This works: (i.e. hits 5000 when fetching data)

1
[karma@tpd|~/d/j/s/simple]  (master) λ REACT_APP_SERVER_URL=http://localhost:5000 PORT=8000 npm start 

but this don’t:

1
[karma@tpd|~/d/j/s/simple]  (master) λ REACT_APP_SERVER_URL=http://localhost:5000 PORT=8000 python3 -m http.server --directory build/ 8080

Investigation

Well, the reason is I should have build the app in proper environment first and then run it even withour that enviroment (previous values would be baked into the build app). So proper sequence of operations is:

1
2
[karma@tpd|~/d/j/s/simple]  (master) λ REACT_APP_SERVER_URL=http://localhost:5000 PORT=8000 npm run build
[karma@tpd|~/d/j/s/simple]   python3 -m http.server --directory build/ 8080

Separate .env

  • there may be more than two .env files (for example: for two different sets of variables which we want to apply for development and production) (see this SO answer)

How to correctly build react app image?

Inspiration: Creating your first react app using Docker

To be honest, I don’t “feel” how shoudl I use Docker:

  • should I build everything locally and docker-compose up only when the build is done or
  • should I leave the building to the container

How to dockerize Spring Boot?

I tried to build java app in Docker container in such a way that I copy pom.xml and .mvnw scripts and then let the maven build the app, however, downloading Spring Boot dependencies took so much time that now I simply build locally the jar and rebuild the image using that changed jar.

The proper way to do this, I guess, is to

  • use layered jar as explained in spring.io blog and then
  • use multi-stage docker file to extract and copy separate layers

Summary

I have learned a lot. I’ve discovered huge areas of things I don’t know. I have practiced React and tried Docker. This is a green field for me, so I was having fun…


Ten wpis jest częścią serii docker.

Wszystkie wpisy w tej serii: