diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..24cd09a --- /dev/null +++ b/Dockerfile @@ -0,0 +1,29 @@ +FROM node:20-bullseye as client + +WORKDIR /usr/src/app/client +COPY ./client/package*.json ./ +RUN npm i +COPY ./client/. . +RUN npm run build + +FROM azul/prime:17 as server + +WORKDIR /usr/src/app/server +COPY ./server/. . +RUN ./gradlew clean build && bash -c "rm build/libs/*-plain.jar && mv build/libs/*.jar server.jar" + + +FROM azul/prime:17 + +WORKDIR /usr/src/app +COPY --from=server /usr/src/app/server/server.jar . +RUN mkdir -p client/dist +COPY --from=client /usr/src/app/client/dist ./client/dist + +ENV spring_profiles_active=prod + +COPY ./dockerentry.sh . + +RUN ["chmod", "+x", "./dockerentry.sh"] + +CMD [ "./dockerentry.sh" ] \ No newline at end of file diff --git a/client/.dockerignore b/client/.dockerignore new file mode 100644 index 0000000..ef718b9 --- /dev/null +++ b/client/.dockerignore @@ -0,0 +1,3 @@ +.parcel-cache +dist +node_modules diff --git a/client/package.json b/client/package.json index 5f9d2d9..d584517 100644 --- a/client/package.json +++ b/client/package.json @@ -5,10 +5,9 @@ "author": "Stepan Usatiuk", "source": "src/index.html", "scripts": { - "start": "parcel", - "build": "parcel build" + "start": "parcel --public-url /app", + "build": "parcel build --public-url /app" }, - "publicUrl": "/app", "browserslist": "> 0.5%, last 2 versions, not dead", "dependencies": { "jwt-decode": "^4.0.0", diff --git a/client/src/api/utils.ts b/client/src/api/utils.ts index 4244db9..fb5694d 100644 --- a/client/src/api/utils.ts +++ b/client/src/api/utils.ts @@ -10,7 +10,7 @@ declare const process: { }; const apiRoot: string = - process.env.NODE_ENV == "production" ? "/" : "http://localhost:8080"; + process.env.NODE_ENV == "production" ? "" : "http://localhost:8080"; let token: string | null; diff --git a/docker-compose.example.yml b/docker-compose.example.yml new file mode 100644 index 0000000..89d9d27 --- /dev/null +++ b/docker-compose.example.yml @@ -0,0 +1,37 @@ +version: "3.8" +services: + yapp: + build: ./ + restart: unless-stopped + ports: + - "8080:8080" + environment: + - jwt_secret=secretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecretsecret + - spring_datasource_url=jdbc:mariadb://db:3306/yapp + - spring_datasource_username=yapp + - spring_datasource_password=yappyapp + depends_on: + db: + condition: service_healthy + db: + image: mariadb + restart: unless-stopped + environment: + - MYSQL_RANDOM_ROOT_PASSWORD=true + - MYSQL_USER=yapp + - MYSQL_PASSWORD=yappyapp + - MYSQL_DATABASE=yapp + - MYSQL_CHARSET=utf8mb4 + - MYSQL_COLLATION=utf8mb4_general_ci + volumes: + - ymariadb:/var/lib/mysql + healthcheck: + test: [ "CMD", "healthcheck.sh", "--connect", "--innodb_initialized" ] + start_period: 30s + start_interval: 10s + interval: 5s + timeout: 5s + retries: 3 + +volumes: + ymariadb: \ No newline at end of file diff --git a/dockerentry.sh b/dockerentry.sh new file mode 100644 index 0000000..9bc2fbd --- /dev/null +++ b/dockerentry.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +java -jar server.jar diff --git a/server/.dockerignore b/server/.dockerignore new file mode 100644 index 0000000..91ea741 --- /dev/null +++ b/server/.dockerignore @@ -0,0 +1,2 @@ +build +.gradle diff --git a/server/build.gradle b/server/build.gradle index 73eb0b4..afab315 100644 --- a/server/build.gradle +++ b/server/build.gradle @@ -44,3 +44,11 @@ dependencies { tasks.named('test') { useJUnitPlatform() } + +jar { + manifest { + attributes( + 'Main-Class': 'com.usatiuk.tjv.y.server.ServerApplication' + ) + } +} \ No newline at end of file diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/WebConfig.java b/server/src/main/java/com/usatiuk/tjv/y/server/WebConfig.java index 3cfc3f2..b3e97d0 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/WebConfig.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/WebConfig.java @@ -2,9 +2,13 @@ package com.usatiuk.tjv.y.server; import jakarta.servlet.http.HttpServletRequest; import org.springframework.context.annotation.Configuration; -import org.springframework.core.io.ClassPathResource; +import org.springframework.core.io.FileSystemResource; import org.springframework.core.io.Resource; import org.springframework.http.CacheControl; +import org.springframework.http.MediaType; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.config.annotation.EnableWebMvc; import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry; import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; @@ -12,6 +16,9 @@ import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; import org.springframework.web.servlet.resource.ResourceResolver; import org.springframework.web.servlet.resource.ResourceResolverChain; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; import java.time.Duration; import java.util.List; @@ -21,9 +28,9 @@ public class WebConfig implements WebMvcConfigurer { static class AppResourceResolver implements ResourceResolver { @Override public Resource resolveResource(HttpServletRequest request, String requestPath, List locations, ResourceResolverChain chain) { - ClassPathResource res = new ClassPathResource("/app/" + requestPath); + FileSystemResource res = new FileSystemResource("/usr/src/app/client/dist/" + requestPath); if (res.exists()) return res; - ClassPathResource indexRes = new ClassPathResource("/app/index.html"); + FileSystemResource indexRes = new FileSystemResource("/usr/src/app/client/dist/index.html"); if (indexRes.exists()) return indexRes; return null; } @@ -34,9 +41,24 @@ public class WebConfig implements WebMvcConfigurer { } } + + @RestController + @RequestMapping(value = "/app", produces = MediaType.TEXT_HTML_VALUE) + static class AppRootContoller { + @GetMapping + public String get() throws IOException { + return Files.readString(Path.of("/usr/src/app/client/dist/index.html")); + } + + @GetMapping("/") + public String getSlash() throws IOException { + return Files.readString(Path.of("/usr/src/app/client/dist/index.html")); + } + } + @Override public void addViewControllers(ViewControllerRegistry registry) { - registry.addRedirectViewController("/app/", "/app/index.html"); + registry.addRedirectViewController("/", "/app"); } @Override diff --git a/server/src/main/java/com/usatiuk/tjv/y/server/security/WebSecurityConfig.java b/server/src/main/java/com/usatiuk/tjv/y/server/security/WebSecurityConfig.java index 1ca6184..e732aa9 100644 --- a/server/src/main/java/com/usatiuk/tjv/y/server/security/WebSecurityConfig.java +++ b/server/src/main/java/com/usatiuk/tjv/y/server/security/WebSecurityConfig.java @@ -63,6 +63,7 @@ public class WebSecurityConfig { .requestMatchers(mvc.pattern("/swagger-ui*/**")).permitAll() .requestMatchers(mvc.pattern("/v3/**")).permitAll() .requestMatchers(mvc.pattern("/error")).permitAll() + .requestMatchers(mvc.pattern("/")).permitAll() .anyRequest().hasAuthority(UserRoles.ROLE_USER.name())) .sessionManagement((session) -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) .addFilterBefore(jwtRequestFilter, UsernamePasswordAuthenticationFilter.class)