Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
**/*.jar
**/target
.idea/
**/**/project.iml
57 changes: 39 additions & 18 deletions edu-spring-postgres/backend/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,26 +1,47 @@
# syntax=docker/dockerfile:1.4
# syntax=docker/dockerfile:1.6

FROM --platform=$BUILDPLATFORM maven:3.9-eclipse-temurin-21 AS builder
WORKDIR /workdir/server
COPY pom.xml /workdir/server/pom.xml
RUN mvn dependency:go-offline
# Stage 1: Build
FROM maven:3.9.14-eclipse-temurin-25-alpine AS builder

COPY src /workdir/server/src
RUN mvn install
RUN apk upgrade --available --no-cache

FROM builder AS prepare-production
RUN mkdir -p target/dependency
WORKDIR /workdir/server/target/dependency
RUN jar -xf ../*.jar
WORKDIR /app

FROM eclipse-temurin:21-jre-noble
COPY pom.xml .

RUN mvn --batch-mode --errors --quiet dependency:go-offline

# Copy the relevant directory
COPY src ./src

# If check passes, build the jar
RUN mvn --batch-mode --errors --quiet clean package -DskipTests

# Extract the jar file using an efficient layout
RUN java -Djarmode=tools -jar target/project.jar extract --layers --destination extracted

# Stage 2: Run the application
FROM eclipse-temurin:25.0.2_10-jre-alpine-3.23

LABEL org.opencontainers.image.source=https://github.com/notalib/workshop-containerisation
LABEL org.opencontainers.image.description="Spring Boot example application"

# Upgrade packages
RUN apk upgrade --available --no-cache

# Copy the extracted jar contents from the builder container into the working directory in the runtime container
# Every copy step creates a new docker layer
# This allows docker to only pull the changes it really needs
COPY --from=builder /app/extracted/dependencies/ ./
COPY --from=builder /app/extracted/spring-boot-loader/ ./
COPY --from=builder /app/extracted/snapshot-dependencies/ ./
COPY --from=builder /app/extracted/application/ ./

EXPOSE 8080
VOLUME /tmp
ARG DEPENDENCY=/workdir/server/target/dependency
COPY --from=prepare-production ${DEPENDENCY}/BOOT-INF/lib /app/lib
COPY --from=prepare-production ${DEPENDENCY}/META-INF /app/META-INF
COPY --from=prepare-production ${DEPENDENCY}/BOOT-INF/classes /app
COPY entrypoint.sh /entrypoint.sh
RUN chmod +x /entrypoint.sh

# Add a non-root user
RUN addgroup --system spring && adduser --system --ingroup spring spring
USER spring:spring

ENTRYPOINT ["/entrypoint.sh"]
10 changes: 9 additions & 1 deletion edu-spring-postgres/backend/entrypoint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,12 @@ if [ -f /run/secrets/db-password ]; then
export POSTGRES_PASSWORD="$(cat /run/secrets/db-password)"
fi

exec java -cp "app:app/lib/*" com.company.project.Application
# Run Spring Boot app
# Start the application jar - this is not the uber jar used by the builder
# This jar only contains application code and references to the extracted jar files
# This layout is efficient to start up and AOT cache (and CDS) friendly
# -XX:+UseContainerSupport is on by default in Java 11+ but explicit is better than implicit.
# -XX:MaxRAMPercentage=75.0 tells the JVM to use up to 75% of the container’s available memory for the heap. Leaving the remaining 25% for native memory, Metaspace, and thread stacks is a reasonable default for most applications.
# -XX:+ExitOnOutOfMemoryError makes the JVM exit (and the container restart) rather than limping along in a degraded state when it runs out of memory

exec java -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0 -XX:+ExitOnOutOfMemoryError -jar project.jar
44 changes: 17 additions & 27 deletions edu-spring-postgres/backend/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,24 @@
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.company</groupId>
<artifactId>project</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>

<name>New App</name>
<description>My new SpringBoot app</description>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.4.13</version>
<version>4.0.6</version>
<relativePath/>
</parent>
<groupId>com.company</groupId>
<artifactId>project</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>New App</name>
<description>My new SpringBoot app</description>
<packaging>jar</packaging>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>21</java.version>
<java.version>25</java.version>
<testcontainers.version>2.0.5</testcontainers.version>
</properties>

<dependencies>
Expand All @@ -44,6 +42,7 @@
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.11</version>
<scope>runtime</scope>
</dependency>
<dependency>
Expand All @@ -60,43 +59,34 @@
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>

<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-junit-jupiter</artifactId>
<version>2.0.5</version>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-resttestclient</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers</artifactId>
<version>2.0.5</version>
<artifactId>testcontainers-junit-jupiter</artifactId>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>testcontainers-postgresql</artifactId>
<version>2.0.1</version>
<version>${testcontainers.version}</version>
<scope>test</scope>
</dependency>

<dependency>
<groupId>org.jsoup</groupId>
<artifactId>jsoup</artifactId>
<version>1.18.3</version>
<version>1.22.2</version>
<scope>test</scope>
</dependency>

</dependencies>

<build>
<finalName>${project.artifactId}</finalName>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
@SpringBootApplication
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
package com.company.project.controller;

import com.company.project.entity.Greeting;
import com.company.project.service.IGreetingService;
import java.util.UUID;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.server.ResponseStatusException;

@Controller
public class GreetingController {

private static final Logger log = LoggerFactory.getLogger(GreetingController.class);

private final IGreetingService iGreetingService;

@Autowired
public GreetingController(IGreetingService iGreetingService) {
this.iGreetingService = iGreetingService;
}

@GetMapping("/")
public String showHome(Model model) {
String greetingDocker = "Docker";
log.info("GET / — loading default greeting with name='{}'", greetingDocker);
Greeting dockerGreeting = iGreetingService.showHome(greetingDocker).orElseThrow(() -> {
log.warn("Greeting with name='{}' not found", greetingDocker);
return new ResponseStatusException(HttpStatus.NOT_FOUND, "Greeting '" + greetingDocker + "' not found");
});
log.info("Loaded greeting: '{}'", dockerGreeting.getName());
model.addAttribute("name", dockerGreeting.getName());
model.addAttribute("body", "Connected to database!");
return "greeting-single";
}

@GetMapping("/greetings")
public String listGreetings(Model model) {
log.info("GET /greetings — listing all greetings");
Iterable<Greeting> greetings = iGreetingService.listGreetings();
model.addAttribute("greetings", greetings);
return "greetings";
}

@GetMapping("/greetings/{id}")
public String sayHello(@PathVariable UUID id, Model model) {
log.info("GET /greetings/{} — looking up greeting", id);
Greeting greeting = iGreetingService.sayHello(id).orElseThrow(() -> {
log.warn("Greeting with id='{}' not found", id);
return new ResponseStatusException(HttpStatus.NOT_FOUND, "Greeting '" + id + "' not found");
});
log.info("Found greeting id='{}' name='{}'", id, greeting.getName());
model.addAttribute("name", greeting.getName());
model.addAttribute("body", "Greeting #" + id);
return "greeting-single";
}

@GetMapping("/new")
public String newGreetingForm() {
return "new";
}

@PostMapping("/greetings")
public String createGreeting(@RequestParam String name) {
log.info("POST /greetings — creating new greeting with name='{}'", name);
Greeting greeting = iGreetingService.createGreeting(name);
log.info("Saved new greeting id='{}' name='{}'", greeting.getId(), greeting.getName());
return "redirect:/greetings/" + greeting.getId();
}
}

This file was deleted.

Loading