Remove nginx and serve Angular directly from Spring Boot (#2662)

* feat: remove nginx and serve Angular directly from Spring Boot

* fix: handle null values in EnabledFields deserialization from persisted JSON

* fix(migration): auto-repair failed Flyway migrations on startup

* fix(migration): replace DB triggers with app-level orphan cleanup

* fix: restore default port to 6060 for backwards compatibility

* fix: align all port references to 6060 and add OPDS compression MIME types

* fix: resolve NG0101 recursive tick error in book browser selection

---------

Co-authored-by: acx10 <acx10@users.noreply.github.com>
This commit is contained in:
ACX
2026-02-08 15:02:25 -07:00
committed by GitHub
parent 843b64969c
commit bc7ba8b933
24 changed files with 172 additions and 219 deletions

View File

@@ -26,6 +26,9 @@ RUN --mount=type=cache,target=/home/gradle/.gradle \
COPY ./booklore-api/src /springboot-app/src
# Copy Angular dist into Spring Boot static resources so it's embedded in the JAR
COPY --from=angular-build /angular-app/dist/booklore/browser /springboot-app/src/main/resources/static
# Inject version into application.yaml using yq
ARG APP_VERSION
RUN apk add --no-cache yq && \
@@ -53,14 +56,14 @@ LABEL org.opencontainers.image.title="BookLore" \
ENV JAVA_TOOL_OPTIONS="-XX:+UseG1GC -XX:MaxGCPauseMillis=200 -XX:+UseStringDeduplication -XX:+UseContainerSupport -XX:MaxRAMPercentage=75.0"
RUN apk update && apk add nginx gettext su-exec
RUN apk update && apk add --no-cache su-exec
COPY ./nginx.conf /etc/nginx/nginx.conf
COPY --from=angular-build /angular-app/dist/booklore/browser /usr/share/nginx/html
COPY entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
COPY --from=springboot-build /springboot-app/build/libs/booklore-api-0.0.1-SNAPSHOT.jar /app/app.jar
COPY start.sh /start.sh
RUN chmod +x /start.sh
EXPOSE 8080 80
ARG BOOKLORE_PORT=6060
EXPOSE ${BOOKLORE_PORT}
CMD ["/start.sh"]
ENTRYPOINT ["entrypoint.sh"]
CMD ["java", "-jar", "/app/app.jar"]

View File

@@ -214,10 +214,9 @@ Create a `.env` file in your project directory:
```ini
# 🎯 BookLore Application Settings
APP_USER_ID=0
APP_GROUP_ID=0
APP_USER_ID=1000
APP_GROUP_ID=1000
TZ=Etc/UTC
BOOKLORE_PORT=6060
# 🗄️ Database Connection (BookLore)
DATABASE_URL=jdbc:mariadb://mariadb:3306/booklore
@@ -252,18 +251,17 @@ services:
- DATABASE_URL=${DATABASE_URL}
- DATABASE_USERNAME=${DB_USER}
- DATABASE_PASSWORD=${DB_PASSWORD}
- BOOKLORE_PORT=${BOOKLORE_PORT}
depends_on:
mariadb:
condition: service_healthy
ports:
- "${BOOKLORE_PORT}:${BOOKLORE_PORT}"
- "6060:6060"
volumes:
- ./data:/app/data
- ./books:/books
- ./bookdrop:/bookdrop
healthcheck:
test: wget -q -O - http://localhost:${BOOKLORE_PORT}/api/v1/healthcheck
test: wget -q -O - http://localhost:6060/api/v1/healthcheck
interval: 60s
retries: 5
start_period: 60s

View File

@@ -0,0 +1,22 @@
package org.booklore.config;
import org.flywaydb.core.api.exception.FlywayValidateException;
import org.springframework.boot.flyway.autoconfigure.FlywayMigrationStrategy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class FlywayConfig {
@Bean
FlywayMigrationStrategy flywayMigrationStrategy() {
return flyway -> {
try {
flyway.migrate();
} catch (FlywayValidateException e) {
flyway.repair();
flyway.migrate();
}
};
}
}

View File

@@ -5,11 +5,17 @@ import org.booklore.interceptor.KomgaCleanInterceptor;
import org.booklore.interceptor.KomgaEnabledInterceptor;
import org.booklore.interceptor.OpdsEnabledInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.core.task.VirtualThreadTaskExecutor;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
import org.springframework.web.servlet.config.annotation.AsyncSupportConfigurer;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.resource.PathResourceResolver;
import java.io.IOException;
import static org.springframework.data.web.config.EnableSpringDataWebSupport.PageSerializationMode.VIA_DTO;
@@ -27,6 +33,22 @@ public class WebMvcConfig implements WebMvcConfigurer {
configurer.setTaskExecutor(new VirtualThreadTaskExecutor("mvc-async-"));
}
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/**")
.addResourceLocations("classpath:/static/")
.resourceChain(true)
.addResolver(new PathResourceResolver() {
@Override
protected Resource getResource(String resourcePath, Resource location) throws IOException {
Resource resource = location.createRelative(resourcePath);
return resource.exists() && resource.isReadable()
? resource
: new ClassPathResource("/static/index.html");
}
});
}
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(opdsEnabledInterceptor)

View File

@@ -204,6 +204,7 @@ public class SecurityConfig {
publicEndpoints.addAll(Arrays.asList(SWAGGER_ENDPOINTS));
}
http
.securityMatcher("/api/**", "/komga/**", "/ws/**")
.cors(cors -> cors.configurationSource(corsConfigurationSource()))
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
@@ -216,6 +217,17 @@ public class SecurityConfig {
return http.build();
}
@Bean
@Order(9)
public SecurityFilterChain staticResourcesSecurityChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.headers(headers -> headers.frameOptions(HeadersConfigurer.FrameOptionsConfig::sameOrigin))
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll());
return http.build();
}
@Bean
public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
AuthenticationManagerBuilder auth = http.getSharedObject(AuthenticationManagerBuilder.class);

View File

@@ -1,5 +1,7 @@
package org.booklore.model.dto.request;
import com.fasterxml.jackson.annotation.JsonSetter;
import com.fasterxml.jackson.annotation.Nulls;
import org.booklore.model.enums.MetadataProvider;
import org.booklore.model.enums.MetadataReplaceMode;
import jakarta.validation.constraints.NotNull;
@@ -88,70 +90,72 @@ public class MetadataRefreshOptions {
@Builder
public static class EnabledFields {
// All fields default to true so metadata fetcher updates fields by default
// unless explicitly disabled by the user
@Builder.Default
// unless explicitly disabled by the user.
// @JsonSetter(nulls = Nulls.SKIP) ensures that null values in persisted JSON
// (e.g. from older versions missing newer fields) are ignored, preserving defaults.
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean title = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean subtitle = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean description = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean authors = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean publisher = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean publishedDate = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean seriesName = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean seriesNumber = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean seriesTotal = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean isbn13 = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean isbn10 = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean language = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean categories = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean cover = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean pageCount = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean asin = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean goodreadsId = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean comicvineId = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean hardcoverId = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean googleId = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean lubimyczytacId = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean amazonRating = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean amazonReviewCount = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean goodreadsRating = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean goodreadsReviewCount = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean hardcoverRating = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean hardcoverReviewCount = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean lubimyczytacRating = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean ranobedbId = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean ranobedbRating = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean moods = true;
@Builder.Default
@Builder.Default @JsonSetter(nulls = Nulls.SKIP)
private boolean tags = true;
/**

View File

@@ -2,10 +2,16 @@ package org.booklore.repository;
import org.booklore.model.entity.ComicCharacterEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import java.util.Optional;
public interface ComicCharacterRepository extends JpaRepository<ComicCharacterEntity, Long> {
Optional<ComicCharacterEntity> findByName(String name);
@Modifying(flushAutomatically = true)
@Query(value = "DELETE FROM comic_character WHERE id NOT IN (SELECT DISTINCT character_id FROM comic_metadata_character_mapping)", nativeQuery = true)
void deleteOrphaned();
}

View File

@@ -2,10 +2,16 @@ package org.booklore.repository;
import org.booklore.model.entity.ComicCreatorEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import java.util.Optional;
public interface ComicCreatorRepository extends JpaRepository<ComicCreatorEntity, Long> {
Optional<ComicCreatorEntity> findByName(String name);
@Modifying(flushAutomatically = true)
@Query(value = "DELETE FROM comic_creator WHERE id NOT IN (SELECT DISTINCT creator_id FROM comic_metadata_creator_mapping)", nativeQuery = true)
void deleteOrphaned();
}

View File

@@ -2,10 +2,16 @@ package org.booklore.repository;
import org.booklore.model.entity.ComicLocationEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import java.util.Optional;
public interface ComicLocationRepository extends JpaRepository<ComicLocationEntity, Long> {
Optional<ComicLocationEntity> findByName(String name);
@Modifying(flushAutomatically = true)
@Query(value = "DELETE FROM comic_location WHERE id NOT IN (SELECT DISTINCT location_id FROM comic_metadata_location_mapping)", nativeQuery = true)
void deleteOrphaned();
}

View File

@@ -2,10 +2,16 @@ package org.booklore.repository;
import org.booklore.model.entity.ComicTeamEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Modifying;
import org.springframework.data.jpa.repository.Query;
import java.util.Optional;
public interface ComicTeamRepository extends JpaRepository<ComicTeamEntity, Long> {
Optional<ComicTeamEntity> findByName(String name);
@Modifying(flushAutomatically = true)
@Query(value = "DELETE FROM comic_team WHERE id NOT IN (SELECT DISTINCT team_id FROM comic_metadata_team_mapping)", nativeQuery = true)
void deleteOrphaned();
}

View File

@@ -146,12 +146,14 @@ public class SettingPersistenceHelper {
.comicvineId(nullProvider)
.hardcoverId(nullProvider)
.googleId(nullProvider)
.lubimyczytacId(nullProvider)
.amazonRating(nullProvider)
.amazonReviewCount(nullProvider)
.goodreadsRating(nullProvider)
.goodreadsReviewCount(nullProvider)
.hardcoverRating(nullProvider)
.hardcoverReviewCount(nullProvider)
.lubimyczytacRating(nullProvider)
.ranobedbId(nullProvider)
.ranobedbRating(nullProvider)
.moods(nullProvider)
@@ -179,12 +181,14 @@ public class SettingPersistenceHelper {
.comicvineId(true)
.hardcoverId(true)
.googleId(true)
.lubimyczytacId(true)
.amazonRating(true)
.amazonReviewCount(true)
.goodreadsRating(true)
.goodreadsReviewCount(true)
.hardcoverRating(true)
.hardcoverReviewCount(true)
.lubimyczytacRating(true)
.ranobedbId(false)
.ranobedbRating(false)
.moods(true)

View File

@@ -438,6 +438,11 @@ public class BookMetadataUpdater {
if (comicDto.getLocationsLocked() != null) c.setLocationsLocked(comicDto.getLocationsLocked());
comicMetadataRepository.save(c);
comicCharacterRepository.deleteOrphaned();
comicTeamRepository.deleteOrphaned();
comicLocationRepository.deleteOrphaned();
comicCreatorRepository.deleteOrphaned();
}
private void updateComicCharacters(ComicMetadataEntity c, Set<String> characters, MetadataReplaceMode mode) {

View File

@@ -20,7 +20,11 @@ app:
server:
forward-headers-strategy: native
port: 8080
port: ${BOOKLORE_PORT:6060}
compression:
enabled: true
mime-types: text/html,text/css,application/javascript,application/json,image/svg+xml,application/xml,application/atom+xml
min-response-size: 1024
tomcat:
relaxed-query-chars: '[,],%,{,},|'

View File

@@ -1,39 +0,0 @@
DROP TRIGGER IF EXISTS trg_cleanup_orphaned_comic_character;
CREATE TRIGGER trg_cleanup_orphaned_comic_character
AFTER DELETE
ON comic_metadata_character_mapping
FOR EACH ROW
DELETE
FROM comic_character
WHERE id = OLD.character_id
AND NOT EXISTS (SELECT 1 FROM comic_metadata_character_mapping WHERE character_id = OLD.character_id);
DROP TRIGGER IF EXISTS trg_cleanup_orphaned_comic_team;
CREATE TRIGGER trg_cleanup_orphaned_comic_team
AFTER DELETE
ON comic_metadata_team_mapping
FOR EACH ROW
DELETE
FROM comic_team
WHERE id = OLD.team_id
AND NOT EXISTS (SELECT 1 FROM comic_metadata_team_mapping WHERE team_id = OLD.team_id);
DROP TRIGGER IF EXISTS trg_cleanup_orphaned_comic_location;
CREATE TRIGGER trg_cleanup_orphaned_comic_location
AFTER DELETE
ON comic_metadata_location_mapping
FOR EACH ROW
DELETE
FROM comic_location
WHERE id = OLD.location_id
AND NOT EXISTS (SELECT 1 FROM comic_metadata_location_mapping WHERE location_id = OLD.location_id);
DROP TRIGGER IF EXISTS trg_cleanup_orphaned_comic_creator;
CREATE TRIGGER trg_cleanup_orphaned_comic_creator
AFTER DELETE
ON comic_metadata_creator_mapping
FOR EACH ROW
DELETE
FROM comic_creator
WHERE id = OLD.creator_id
AND NOT EXISTS (SELECT 1 FROM comic_metadata_creator_mapping WHERE creator_id = OLD.creator_id);

View File

@@ -3,7 +3,8 @@
"index": "/index.html",
"navigationUrls": [
"/**",
"!/api/**"
"!/api/**",
"!/komga/**"
],
"assetGroups": [
{

View File

@@ -1,4 +1,4 @@
import {AfterViewInit, ApplicationRef, ChangeDetectorRef, Component, HostListener, inject, NgZone, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {AfterViewInit, ChangeDetectorRef, Component, HostListener, inject, OnDestroy, OnInit, ViewChild} from '@angular/core';
import {ActivatedRoute, NavigationStart, Router} from '@angular/router';
import {ConfirmationService, MenuItem, MessageService} from 'primeng/api';
import {PageTitleService} from '../../../../shared/service/page-title.service';
@@ -105,8 +105,6 @@ export class BookBrowserComponent implements OnInit, AfterViewInit, OnDestroy {
protected appSettingsService = inject(AppSettingsService);
private cdr = inject(ChangeDetectorRef);
private ngZone = inject(NgZone);
private appRef = inject(ApplicationRef);
private activatedRoute = inject(ActivatedRoute);
private router = inject(Router);
private messageService = inject(MessageService);
@@ -477,11 +475,8 @@ export class BookBrowserComponent implements OnInit, AfterViewInit, OnDestroy {
this.bookSelectionService.selectedBooks$
.pipe(takeUntil(this.destroy$))
.subscribe(selectedBooks => {
this.ngZone.run(() => {
this.selectedCount = selectedBooks.size;
this.cdr.detectChanges();
this.appRef.tick();
});
this.selectedCount = selectedBooks.size;
this.cdr.detectChanges();
});
}

View File

@@ -1,7 +1,7 @@
export const environment = {
production: false,
API_CONFIG: {
BASE_URL: 'http://localhost:8080',
BROKER_URL: 'ws://localhost:8080/ws',
BASE_URL: 'http://localhost:6060',
BROKER_URL: 'ws://localhost:6060/ws',
},
};

View File

@@ -3,7 +3,7 @@ services:
image: gradle:9.3.1-jdk25-alpine
command: sh -c "cd /booklore-api && ./gradlew bootRun"
ports:
- "${BACKEND_PORT:-8080}:8080"
- "${BACKEND_PORT:-6060}:6060"
- "${REMOTE_DEBUG_PORT:-5005}:5005"
environment:
- DATABASE_URL=jdbc:mariadb://backend_db:3306/booklore

15
entrypoint.sh Normal file
View File

@@ -0,0 +1,15 @@
#!/bin/sh
set -e
USER_ID="${USER_ID:-1000}"
GROUP_ID="${GROUP_ID:-1000}"
# Create group and user if they don't exist
if ! getent group "$GROUP_ID" >/dev/null 2>&1; then
addgroup -g "$GROUP_ID" -S booklore
fi
if ! getent passwd "$USER_ID" >/dev/null 2>&1; then
adduser -u "$USER_ID" -G "$(getent group "$GROUP_ID" | cut -d: -f1)" -S -D booklore
fi
exec su-exec "$USER_ID:$GROUP_ID" "$@"

View File

@@ -62,8 +62,6 @@ spec:
containerPort: {{ .Values.service.port }}
#protocol: TCP
env:
- name: BOOKLORE_PORT
value: "6060"
- name: DATABASE_URL
value: jdbc:mariadb://{{ .Release.Name }}-mariadb:{{ .Values.mariadb.primary.service.ports.mysql }}/{{ .Values.mariadb.auth.database }}
- name: DATABASE_USERNAME

View File

@@ -6,22 +6,19 @@ services:
# image: ghcr.io/booklore-app/booklore:latest
container_name: booklore
environment:
- USER_ID=0 # Modify this if the volume's ownership is not root
- GROUP_ID=0 # Modify this if the volume's ownership is not root
- USER_ID=1000 # Modify this if the volume's ownership is not root
- GROUP_ID=1000 # Modify this if the volume's ownership is not root
- TZ=Etc/UTC
- DATABASE_URL=jdbc:mariadb://mariadb:3306/booklore # Only modify this if you're familiar with JDBC and your database setup
- DATABASE_USERNAME=booklore # Must match MYSQL_USER defined in the mariadb container
- DATABASE_PASSWORD=your_secure_password # Use a strong password; must match MYSQL_PASSWORD defined in the mariadb container
- BOOKLORE_PORT=6060 # Port BookLore listens on inside the container; must match container port below
- DATABASE_PASSWORD=your_secure_password # Use a strong password; must match MYSQL_PASSWORD defined in the mariadb container
- SWAGGER_ENABLED=false # Enable or disable Swagger UI (API docs). Set to 'true' to allow access; 'false' to block access (recommended for production).
- FORCE_DISABLE_OIDC=false # Set to 'true' to force-disable OIDC and allow internal login, regardless of UI config
depends_on:
mariadb:
condition: service_healthy
ports:
- "6060:6060" # HostPort:ContainerPort → Keep both numbers the same, and also ensure the container port matches BOOKLORE_PORT, no exceptions.
# All three (host port, container port, BOOKLORE_PORT) must be identical for BookLore to function properly.
# Example: To expose on host port 7070, set BOOKLORE_PORT=7070 and use "7070:7070".
- "6060:6060" # HostPort:ContainerPort — container port is 6060 by default (configurable via BOOKLORE_PORT env var)
volumes:
- ./data:/app/data # Application data (settings, metadata, cache, etc.). Persist this folder to retain your library state across container restarts.
- ./books:/books # Primary book library folder. Mount your collection here so BookLore can access and organize your books.

View File

@@ -20,8 +20,6 @@ Environment=TZ=Etc/UTC
Secret=booklore_db_pass,type=env,target=DATABASE_PASSWORD
Environment=DATABASE_URL=jdbc:mariadb://localhost:3306/booklore
Environment=DATABASE_USERNAME=booklore
Environment=BOOKLORE_PORT=6060
HealthCmd=wget -q -O - http://localhost:6060/api/v1/healthcheck
HealthInterval=1m
HealthRetries=5

View File

@@ -1,89 +0,0 @@
# the events block is required
events {}
http {
# include the default mime.types to map file extensions to MIME types
include /etc/nginx/mime.types;
# Set max request body size to 100MB (adjust as needed)
client_max_body_size 1000M;
proxy_buffer_size 128k;
proxy_buffers 4 256k;
proxy_busy_buffers_size 256k;
large_client_header_buffers 8 32k;
server {
listen ${BOOKLORE_PORT};
listen [::]:${BOOKLORE_PORT};
# Set the root directory for the server (Angular app)
root /usr/share/nginx/html;
# Set the default index file for the server
index index.html;
# Serve Angular UI
location / {
try_files $uri $uri/ /index.html; # Fallback to index.html for Angular routing
location ~* \.mjs$ {
# target only *.mjs files
# now we can safely override types since we are only
# targeting a single file extension.
types {
text/javascript mjs;
}
}
}
# Proxy API requests that start with /api/ to the backend
location /api/ {
proxy_pass http://localhost:8080;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Host $host;
# Disable buffering for streaming responses
proxy_buffering off;
proxy_cache off;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding on;
# Set timeouts for long-running connections
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
# Proxy API requests that start with /komga/ to the backend
# we can't use /api because Komga clients expect /komga/api/v1
location /komga/ {
proxy_pass http://localhost:8080;
proxy_set_header X-Forwarded-Port $server_port;
proxy_set_header X-Forwarded-Host $host;
# Disable buffering for streaming responses
proxy_buffering off;
proxy_cache off;
proxy_set_header Connection '';
proxy_http_version 1.1;
chunked_transfer_encoding on;
# Set timeouts for long-running connections
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
# Proxy WebSocket requests (ws://) to the backend
location /ws {
proxy_pass http://localhost:8080/ws; # Backend WebSocket endpoint
proxy_http_version 1.1; # Ensure HTTP 1.1 is used for WebSocket connection
proxy_set_header Upgrade $http_upgrade; # Pass the upgrade header
proxy_set_header Connection 'upgrade'; # Pass the connection header
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto $scheme;
}
}
}

View File

@@ -1,21 +0,0 @@
#!/bin/sh
# Set default and export so envsubst sees it
: "${BOOKLORE_PORT:=6060}"
export BOOKLORE_PORT
# Use envsubst safely
TMP_CONF="/tmp/nginx.conf.tmp"
envsubst '${BOOKLORE_PORT}' < /etc/nginx/nginx.conf > "$TMP_CONF"
# Move to final location
mv "$TMP_CONF" /etc/nginx/nginx.conf
# Disable nginx IPv6 listener when IPv6 is disabled on host
[ "$(cat /proc/sys/net/ipv6/conf/all/disable_ipv6 2>/dev/null)" = "0" ] || sed -i '/^[[:space:]]*listen \[\:\:\]:6060;$/d' /etc/nginx/nginx.conf
# Start nginx in background
nginx -g 'daemon off;' &
# Start Spring Boot in foreground
su-exec ${USER_ID:-0}:${GROUP_ID:-0} java -jar /app/app.jar