update cicd flow with simplesoft-duongdt3/komga

This commit is contained in:
duong.doan1 2026-04-09 10:52:42 +07:00
parent c97b103b5b
commit 99b509d013
22 changed files with 1509 additions and 66 deletions

8
.dockerignore Normal file
View file

@ -0,0 +1,8 @@
**/node_modules
**/.git
**/.gradle
**/build
**/dist
**/.idea
**/.vscode
*.log

View file

@ -1,23 +0,0 @@
name: Update DockerHub description
on:
push:
branches:
- master
paths:
- 'DOCKERHUB.md'
jobs:
update_docker_description:
name: Update DockerHub description
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@master
- name: DockerHub Description
uses: peter-evans/dockerhub-description@v5.0.0
env:
DOCKERHUB_USERNAME: ${{ secrets.DOCKER_USERNAME }}
DOCKERHUB_PASSWORD: ${{ secrets.DOCKER_PASSWORD }}
DOCKERHUB_REPOSITORY: gotson/komga
README_FILEPATH: ./DOCKERHUB.md

View file

@ -27,7 +27,7 @@ on:
default: true
type: boolean
docker_release:
description: 'Push Docker images'
description: 'Push Docker images to GitHub Container Registry'
default: true
type: boolean
msstore_release:
@ -103,13 +103,7 @@ jobs:
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
- name: Login to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Login to Docker Hub
- name: Login to GitHub Container Registry
uses: docker/login-action@v4
with:
registry: ghcr.io

View file

@ -0,0 +1,727 @@
# Plan: Optimize Dockerfile.local Build Performance
## Problem Statement
Current `Dockerfile.local` builds both frontend (FE) and backend (BE) in a single stage, resulting in long build times. The FE build involves `npm install` and `npm run build`, which are timeconsuming and are triggered even when only BE code changes.
## Objectives
- Reduce overall build time for production image builds using `Dockerfile.local`
- Maximize Docker layer caching, especially for the frontend dependencies (`node_modules`)
- Maintain compatibility with existing development and CI workflows
- Ensure the final image remains functionally identical
## Current Analysis
### Usage Context
- `Dockerfile.local` is **only for local development**, not used in CI production builds.
- CI/CD uses `komga/docker/Dockerfile.tpl` via JReleaser for multiarch production images.
- The main consumption is through `buildlocaldocker.sh` script and `dockercompose.local.yml`.
- No other scripts or workflows depend on the exact structure of `Dockerfile.local`.
### Dockerfile.local Structure
1. **Single build stage** based on `eclipsetemurin:21jdk`
2. Installs Node.js 18 systemwide
3. Copies **all** source code (including `komgawebui`) before running any build steps
4. Runs `./gradlew :komga:prepareThymeLeaf :komga:bootJar`
- `prepareThymeLeaf` depends on `npmInstall``npmBuild``copyWebDist`
- This triggers a full frontend build on every Docker build
### Pain Points
- `npm install` runs on every build because the `COPY` of the entire source tree invalidates the cache layer.
- Changes to any source file (BE or FE) cause the frontend build to rerun.
- No separation between FE and BE caching.
- Frontend build (npm install + npm run build) is the most timeconsuming part.
## Proposed Solution
### Multistage Build with Separate FE Stage
Introduce a dedicated frontend build stage that caches `node_modules` independently.
```
Stage 1: frontendbuilder
- Use `node:18alpine` as base
- Copy only `komgawebui/package*.json`
- Run `npm ci` (or `npm install`) → cached layer
- Copy the rest of `komgawebui`
- Run `npm run build`
- Output: `/app/dist` directory
Stage 2: backendbuilder
- Use `eclipsetemurin:21jdk` as base
- Copy Gradle files (`gradle/`, `gradlew`, `*.gradle.kts`, etc.)
- Copy backend source (`komga/`, `komgatray/`)
- Copy the built frontend dist from Stage 1 into `komga/src/main/resources/public/`
- Optionally inject ThymeLeaf tags (can be done with a simple `sed` or keep the Gradle task)
- Run `./gradlew :komga:bootJar` (skip `prepareThymeLeaf` because dist is already present)
Stage 3: runtime
- Use `eclipsetemurin:21jrealpine`
- Copy the JAR from Stage 2
- Set up nonroot user and entrypoint
```
### Cache Optimization Details
1. **Frontend dependencies**: Layer depends only on `package*.json`. Changes to FE source code will not trigger `npm install` again.
2. **Frontend build**: Layer depends on the whole `komgawebui` source, but `node_modules` is cached.
3. **Backend dependencies**: Gradle cache can be preserved by copying `~/.gradle/caches` (optional, but can be added as a volume in CI).
4. **Backend build**: Copying the prebuilt frontend dist eliminates the need to run `npm` tasks inside the backend stage.
### Alternative: TwoPhase Gradle Build
Keep the singlestage approach but reorder `COPY` instructions:
- Copy `komgawebui/package*.json` first, run `npm install`
- Copy the rest of the source
- Run `./gradlew :komga:prepareThymeLeaf :komga:bootJar`
This still runs the Gradle task that triggers `npmBuild`, but at least `npm install` is cached.
## Implementation Steps
1. **Analyze existing Gradle tasks** to ensure skipping `prepareThymeLeaf` is safe.
- Check if any other task depends on `prepareThymeLeaf` (e.g., `bootJar`).
- Verify that `bootJar` includes the `public/` directory.
2. **Create new Dockerfile.local.optimized** (or modify the existing one) with the multistage design.
- Write the frontend stage using `node:18alpine`.
- Ensure the ThymeLeaf tag injection is performed (either in the frontend stage or via a small script).
- Test that the built JAR contains the correct frontend assets.
3. **Update dockercompose.local.yml** (if necessary) to reference the new Dockerfile.
4. **Validate build time improvement** with a sample workflow:
- Clean build
- Rebuild after changing a backend source file
- Rebuild after changing a frontend source file
- Rebuild after changing `package.json`
5. **Ensure compatibility** with existing CI/CD (GitHub Actions) and local development commands.
## Risks & Mitigations
| Risk | Mitigation |
|------|------------|
| Skipping `prepareThymeLeaf` may break ThymeLeaf variable injection | Perform the same injection in the frontend stage using a simple script (or keep the Gradle task but make it depend only on the copied dist). |
| Docker layer caching may not work as expected in CI environments | Use `--cachefrom` flags and consider storing `node_modules` as a separate buildcache target. |
| Increased complexity of the Dockerfile | Document each stage clearly and add comments. |
## Success Metrics
- **Build time reduction**: At least 50% faster on clean builds (frontend dependencies already cached).
- **Cache efficiency**: Changing a backend source file should not trigger frontend `npm install` or `npm build`.
- **Image size**: Should not increase significantly.
## Deliverables
1. Optimized `Dockerfile.local` (or `Dockerfile.local.optimized`).
2. Updated documentation (if needed) in `aidocs/dockersetup.md`.
3. Validation script or instructions to verify the improvement.
## Implementation Choices & Decisions Made
### Chosen Approach: Option A Multistage with separate FE stage
- **Rationale**: Maximum caching separation, frontend dependencies cached independently, clean separation of concerns.
- **ThymeLeaf injection**: Will be performed in the frontend stage using a script that mimics the Gradle task's regex replacement.
### File Strategy
- Create new `Dockerfile.local.optimized` alongside the existing `Dockerfile.local`.
- Existing workflows continue to use the old file until migrated.
### Script Updates
- Create separate optimized versions:
- `dockercompose.local.optimized.yml` that references `Dockerfile.local.optimized`.
- Update `buildlocaldocker.sh` to optionally support the optimized version (via a flag or a separate script).
- This allows gradual migration and testing.
## Detailed Implementation Plan
### Phase 1: Create Optimized Dockerfile
1. Write `Dockerfile.local.optimized` with three stages:
- **frontendbuilder**: `node:18alpine`, copy `package*.json`, `npm ci`, copy source, `npm run build`, inject ThymeLeaf tags.
- **backendbuilder**: `eclipsetemurin:21jdk`, copy Gradle files, copy backend source, copy prebuilt frontend dist, run `./gradlew :komga:bootJar`.
- **runtime**: `eclipsetemurin:21jrealpine`, copy JAR, set up user and entrypoint.
2. Implement ThymeLeaf tag injection script (shell or Node.js) that replicates the Gradle filter:
```bash
sed -i -E 's/((?:src|content|href)=")([\w]*/.*?)(")/\1 th:\2@{/\3}/g' index.html
```
(Exact regex to be validated against the Gradle task.)
### Phase 2: Create Supporting Files
1. Create `dockercompose.local.optimized.yml` that points to `dockerfile: Dockerfile.local.optimized`.
2. Optionally create `buildlocaldockeroptimized.sh` or modify the existing script to accept a `--optimized` flag.
### Phase 3: Validation
1. Build the optimized image and verify it runs correctly.
2. Compare build times:
- Clean build (should be similar or slightly faster due to separate stage caching).
- Rebuild after backend source change (should skip frontend `npm install` and `npm run build`).
- Rebuild after frontend source change (should skip `npm install`).
- Rebuild after `package.json` change (should rerun `npm ci` but not backend build).
3. Verify ThymeLeaf tags are correctly injected and the web UI loads.
### Phase 4: Documentation
1. Update `aidocs/dockersetup.md` to mention the optimized option.
2. Add a brief comparison of build times.
## Risks & Mitigations (Updated)
| Risk | Mitigation |
|------|------------|
| ThymeLeaf injection regex mismatch | Test with actual `index.html` from build and compare with output of Gradle task. |
| Docker layer caching not effective due to copy ordering | Carefully order `COPY` commands to maximize cache hits. |
| Increased image size due to multiple stages | Use `alpine` variants and clean up unnecessary files in each stage. |
| Compatibility with existing local development workflows | Keep old files unchanged; new files are optin. |
## Success Metrics
- **Build time reduction**: At least 50% faster on incremental builds (backendonly changes).
- **Cache efficiency**: Changing a backend source file does not trigger `npm install` or `npm run build`.
- **Image size**: Should not increase by more than 5%.
## Deliverables
1. `Dockerfile.local.optimized`
2. `dockercompose.local.optimized.yml`
3. Optional: updated `buildlocaldocker.sh` or new `buildlocaldockeroptimized.sh`
4. Validation results (timing comparisons)
5. Updated documentation in `aidocs/dockersetup.md`
## Brainstorm: Split FE and BE into Two Separate Docker Images
### Current Architecture Analysis
- Komga is a monolithic Spring Boot app with embedded frontend assets
- Frontend (Vue.js) built into `komga-webui/dist/` → copied to `komga/src/main/resources/public/`
- Backend serves:
- API endpoints under `/api/**`
- OPDS feeds under `/opds/**`
- Server-Sent Events under `/sse/**`
- Static assets from `classpath:public/` (frontend)
- SPA fallback: non-API routes forward to `index.html`
- Frontend already configurable via `VUE_APP_KOMGA_API_URL` environment variable
- CORS support exists for development (`dev` profile)
### Proposed Split Options
#### Option 1: Full Separation with Reverse Proxy
- **FE Image**: `nginx:alpine` serving static files from `komga-webui/dist/`
- **BE Image**: Spring Boot JAR only, no embedded frontend assets
- **Reverse Proxy**: nginx/traefik/Caddy routing:
- `/`, `/css/*`, `/js/*`, `/img/*`, `/fonts/*` → FE container
- `/api/**`, `/opds/**`, `/sse/**` → BE container
- **Pros**:
- Complete separation, independent scaling
- FE can be served via CDN
- BE image smaller (no frontend assets)
- Clear separation of concerns
- **Cons**:
- More complex deployment (3 containers)
- Need to manage reverse proxy configuration
- Session/cookie auth may need CORS configuration
#### Option 2: FE as Separate Container, BE Still Serves Assets
- **FE Image**: Frontend build only, mounted as volume to BE container
- **BE Image**: Spring Boot with frontend assets mounted at runtime
- **Setup**: Docker volume mount `fe-dist:/app/resources/public`
- **Pros**:
- Minimal changes to BE code
- FE can be updated independently
- Simpler than full reverse proxy
- **Cons**:
- Still single point of entry (BE)
- Volume mounting complexity
- Less clean separation
#### Option 3: Hybrid - Conditional Serving
- **FE Image**: Standalone nginx serving frontend
- **BE Image**: Spring Boot with configurable static asset serving (enabled/disabled via profile)
- **Routing**: Either reverse proxy or frontend proxies API calls (client-side routing)
- **Pros**:
- Flexible deployment options
- Can run in both monolithic and separated modes
- Gradual migration path
- **Cons**:
- More configuration options to maintain
- Testing complexity
#### Option 4: Build-Time Separation Only
- Keep single Docker image but build FE and BE separately
- FE assets copied into BE image at build time (current multi-stage optimized approach)
- **Pros**:
- Simple, maintains current architecture
- Good caching as implemented
- No runtime complexity
- **Cons**:
- Not true separation
- Can't update FE independently
### Technical Considerations
#### Frontend Changes Needed:
1. **API URL configuration**: Already via `VUE_APP_KOMGA_API_URL`
2. **Base path handling**: Vue Router uses `window.resourceBaseUrl`
3. **Build process**: Need to inject environment variables at build time
4. **Dockerfile**: Create `Dockerfile.fe` with nginx/alpine
#### Backend Changes Needed:
1. **Conditional static resource serving**: Profile or property to disable
2. **CORS configuration**: Enable for FE origin
3. **SPA fallback**: Disable when FE served separately
4. **Remove frontend assets from JAR**: Optional optimization
#### Deployment Options:
1. **Docker Compose**: 3 services (fe, be, reverse-proxy)
2. **Kubernetes**: Separate deployments + ingress
3. **Single host with port mapping**: FE on port 80, BE on port 25600, nginx routing
### Evaluation of Solutions
#### Solution A: Multi-stage Single Image (Already Implemented)
- **What**: `Dockerfile.local.optimized` with separate FE/BE build stages, single runtime image
- **Build time impact**: High improvement for incremental builds
- **Deployment**: Single container, simple
- **Pros**:
- Already implemented and working
- Maximum Docker layer caching
- No architectural changes
- Compatible with all existing setups
- **Cons**:
- Still monolithic deployment
- Can't update FE independently
- Image contains both FE and BE
#### Solution B: Two Images, Reverse Proxy
- **What**: FE image (nginx), BE image (Spring Boot), reverse proxy (nginx/traefik)
- **Build time**: FE and BE build independently, parallel builds possible
- **Deployment**: 3 containers, more complex
- **Pros**:
- True separation of concerns
- Independent scaling and updates
- FE can be served via CDN
- BE image smaller (no frontend assets)
- Modern microservices architecture
- **Cons**:
- Significant architectural change
- Reverse proxy configuration needed
- CORS and authentication complexity
- More moving parts
#### Solution C: Two Images, FE Proxying to BE
- **What**: FE image (nginx with proxy_pass to BE), BE image (Spring Boot)
- **Build time**: Similar to Solution B
- **Deployment**: 2 containers, FE acts as reverse proxy
- **Pros**:
- Simpler than separate reverse proxy
- FE controls routing
- Single entry point
- **Cons**:
- FE container more complex (nginx config)
- FE needs to be redeployed for BE URL changes
- Less flexible than dedicated reverse proxy
#### Solution D: Build-Time Separation, Runtime Combined
- **What**: Build FE and BE separately, but combine in final image
- **Build time**: Similar to Solution A
- **Deployment**: Single container
- **Pros**:
- Clean build separation
- Can version FE and BE independently
- Single deployment unit
- **Cons**:
- Still monolithic runtime
- Complex build pipeline
### Technical Deep Dive: What Would Change
#### For Solution B (Recommended for True Separation):
**Frontend Dockerfile** (`Dockerfile.fe`):
```dockerfile
FROM node:18-alpine AS builder
WORKDIR /app
COPY komga-webui/package*.json ./
RUN npm ci
COPY komga-webui/ ./
ARG VUE_APP_KOMGA_API_URL=http://backend:25600
ENV VUE_APP_KOMGA_API_URL=${VUE_APP_KOMGA_API_URL}
RUN npm run build
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
COPY komga-webui/nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
```
**Backend Dockerfile** (`Dockerfile.be`):
```dockerfile
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app
COPY gradle/ ./gradle
COPY gradlew ./
COPY gradle.properties ./
COPY settings.gradle ./
COPY build.gradle.kts ./
COPY komga/ ./komga
COPY komga-tray/ ./komga-tray
# Don't copy frontend assets
RUN ./gradlew :komga:bootJar -x test -x generateGitProperties -x prepareThymeLeaf
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/komga/build/libs/komga-*.jar komga.jar
# Enable CORS and disable static serving via environment
ENV SPRING_PROFILES_include=docker,separated-fe
RUN addgroup -S komga && adduser -S komga -G komga
USER komga
EXPOSE 25600
ENTRYPOINT ["java", "-Dspring.profiles.include=docker,separated-fe", "--enable-native-access=ALL-UNNAMED", "-jar", "komga.jar"]
```
**Reverse Proxy Configuration** (nginx):
```nginx
server {
listen 80;
# Frontend static files
location / {
proxy_pass http://frontend:80;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
# Backend API
location /api/ {
proxy_pass http://backend:25600;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /opds/ {
proxy_pass http://backend:25600;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /sse/ {
proxy_pass http://backend:25600;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
```
**Backend Code Changes Needed**:
1. New Spring profile `separated-fe` that:
- Disables `ResourceNotFoundController` SPA fallback
- Configures CORS for FE origin
- Optionally skips static resource registration
2. Update `application.yml` with conditional configuration
### Technical Implementation Deep Dive
#### Frontend Changes (All Options)
**Build Configuration** (`komga-webui/`):
1. **Environment variables**: Already supports `VUE_APP_KOMGA_API_URL` in `urls.ts`
2. **Base path**: `window.resourceBaseUrl` set by Thymeleaf injection; need alternative for standalone FE
3. **Build-time injection**: Could use `envsubst` or templating to replace placeholders in `index.html`
4. **Docker multi-stage build**: Need to install dependencies, build, and copy to nginx
**Potential issues**:
- Frontend needs to know backend URL at build time (unless using runtime config)
- Authentication cookies may need `SameSite=None; Secure` for cross-origin
- WebSocket/SSE connections need proper CORS headers
#### Backend Changes (Option B - Full Separation)
**Spring Profile `separated-fe`**:
```kotlin
@Configuration
@Profile("separated-fe")
class SeparatedFeConfiguration {
@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
val configuration = CorsConfiguration()
configuration.allowedOrigins = listOf("http://frontend:80", "http://localhost:8081")
configuration.allowedMethods = listOf("GET", "POST", "PUT", "DELETE", "OPTIONS")
configuration.allowCredentials = true
configuration.allowedHeaders = listOf("*")
val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/api/**", configuration)
source.registerCorsConfiguration("/opds/**", configuration)
source.registerCorsConfiguration("/sse/**", configuration)
return source
}
@Bean
@ConditionalOnMissingBean
fun resourceNotFoundController(): ResourceNotFoundController? {
// Return null bean to disable SPA fallback
return null
}
}
```
**Conditional Static Resource Registration**:
- Modify `WebMvcConfiguration` to check for profile/property
- Skip `addResourceHandlers` registration when `komga.webui.enabled=false`
**Application Configuration**:
```yaml
# application-separated-fe.yml
komga:
webui:
enabled: false
cors:
allowed-origins: ${FE_ORIGIN:http://frontend:80}
```
#### Docker Configuration Details
**Frontend Dockerfile** (`Dockerfile.fe`):
```dockerfile
# Stage 1: Build
FROM node:18-alpine AS builder
WORKDIR /app
COPY package*.json ./
RUN npm ci
COPY . .
ARG VUE_APP_KOMGA_API_URL
ENV VUE_APP_KOMGA_API_URL=${VUE_APP_KOMGA_API_URL}
RUN npm run build
# Stage 2: Runtime
FROM nginx:alpine
COPY --from=builder /app/dist /usr/share/nginx/html
# Custom nginx config for SPA fallback
COPY nginx.conf /etc/nginx/conf.d/default.conf
# Health check
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:80/ || exit 1
EXPOSE 80
```
**Frontend nginx.conf**:
```nginx
server {
listen 80;
server_name localhost;
root /usr/share/nginx/html;
index index.html;
# Gzip compression
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
# SPA fallback
location / {
try_files $uri $uri/ /index.html;
}
# Cache static assets
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2|ttf|eot)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
}
```
**Backend Dockerfile** (`Dockerfile.be`):
```dockerfile
# Stage 1: Build
FROM eclipse-temurin:21-jdk AS builder
WORKDIR /app
# Copy Gradle files
COPY gradle/ ./gradle
COPY gradlew ./
COPY gradle.properties ./
COPY settings.gradle ./
COPY build.gradle.kts ./
# Copy only backend source
COPY komga/ ./komga
COPY komga-tray/ ./komga-tray
# Build without frontend tasks
RUN ./gradlew :komga:bootJar -x test -x generateGitProperties -x prepareThymeLeaf -x npmInstall -x npmBuild -x copyWebDist
# Stage 2: Runtime
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
COPY --from=builder /app/komga/build/libs/komga-*.jar komga.jar
# Environment variables for separated mode
ENV SPRING_PROFILES_ACTIVE=docker,separated-fe
ENV FE_ORIGIN=http://frontend:80
# Non-root user
RUN addgroup -S komga && adduser -S komga -G komga
USER komga
EXPOSE 25600
HEALTHCHECK --interval=30s --timeout=3s --start-period=10s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:25600/api/v1/health || exit 1
ENTRYPOINT ["java", "-Dspring.profiles.active=${SPRING_PROFILES_ACTIVE}", "--enable-native-access=ALL-UNNAMED", "-jar", "komga.jar"]
```
**Reverse Proxy Dockerfile** (`Dockerfile.reverse-proxy`):
```dockerfile
FROM nginx:alpine
COPY nginx-reverse-proxy.conf /etc/nginx/nginx.conf
EXPOSE 80
```
**Docker Compose** (`docker-compose.separated.yml`):
```yaml
version: '3.8'
services:
frontend:
build:
context: .
dockerfile: Dockerfile.fe
args:
VUE_APP_KOMGA_API_URL: http://backend:25600
environment:
- VUE_APP_KOMGA_API_URL=http://backend:25600
networks:
- komga-network
depends_on:
- backend
backend:
build:
context: .
dockerfile: Dockerfile.be
environment:
- SPRING_PROFILES_ACTIVE=docker,separated-fe
- FE_ORIGIN=http://frontend:80
networks:
- komga-network
volumes:
- komga_config:/config
- ./data:/data:ro
reverse-proxy:
build:
context: .
dockerfile: Dockerfile.reverse-proxy
ports:
- "25600:80"
networks:
- komga-network
depends_on:
- frontend
- backend
volumes:
komga_config:
networks:
komga-network:
driver: bridge
```
#### Build Pipeline Changes
**CI/CD Considerations**:
1. **Parallel builds**: FE and BE can build simultaneously
2. **Version tagging**: Need consistent versioning across images (e.g., `komga:1.2.3-be`, `komga:1.2.3-fe`)
3. **Dependency caching**: `node_modules` and Gradle caches should be preserved between builds
4. **Multi-arch builds**: Both images need multi-architecture support for production
**Local Development**:
1. **Hot reload**: FE development server (`npm run serve`) with proxy to backend
2. **Mixed mode**: Option to run FE standalone + BE with embedded assets for debugging
3. **Profile switching**: Easy toggle between monolithic and separated modes
#### Migration Strategy
**Phase 1 - Coexistence**:
- Keep existing `Dockerfile.local` and `Dockerfile.local.optimized`
- Add new Dockerfiles as experimental
- Document both approaches
**Phase 2 - Feature Flags**:
- Add Spring properties to control static serving
- Make CORS configurable via environment
- Update frontend to read config from multiple sources
**Phase 3 - Gradual Migration**:
- Update documentation with new recommended approach
- Provide migration scripts for existing users
- Collect feedback from early adopters
**Phase 4 - Deprecation** (optional):
- Mark monolithic mode as deprecated
- Encourage new deployments to use separated architecture
### Recommendation
**For most users**: **Solution A (multi-stage single image)** is sufficient and already implemented. It provides:
- 50%+ faster incremental builds
- No architectural complexity
- Simple deployment
- Backward compatibility
**For advanced deployments**: **Solution B (two images + reverse proxy)** if you need:
- Independent scaling of FE/BE
- CDN for static assets
- Microservices architecture
- Team separation (frontend/backend teams)
**Implementation Priority**:
1. **Short term**: Use Solution A (already done)
2. **Medium term**: If needed, implement Solution B as optional configuration
3. **Long term**: Maintain both options with feature flags
### Decision Matrix
| Factor | Solution A | Solution B | Solution C |
|--------|------------|------------|------------|
| Build speed | ✅ Excellent | ✅ Good | ✅ Good |
| Deployment simplicity | ✅ Excellent | ⚠ Moderate | ⚠ Moderate |
| Independent updates | ❌ No | ✅ Yes | ⚠ Partial |
| Image size | ⚠ Medium | ✅ Small | ✅ Small |
| Architectural change | ❌ None | ✅ Significant | ⚠ Moderate |
| Learning curve | ✅ None | ⚠ High | ⚠ Medium |
| Production readiness | ✅ Today | ⚠ Requires work | ⚠ Requires work |
### Next Steps
Based on the brainstorming, which direction makes the most sense for your use case?
### Recommended Approach
Based on the analysis, **Option 1 (Full Separation with Reverse Proxy)** offers the cleanest architecture long-term, but **Option 3 (Hybrid)** provides more flexibility during transition.
**Suggested phased implementation**:
**Phase 1**: Create standalone FE Docker image + update BE to conditionally disable static serving
**Phase 2**: Add reverse proxy for local development/demo
**Phase 3**: Update CI/CD to build both images
**Phase 4**: Documentation and migration guide
### Technical Brainstorming Questions
Based on the detailed technical analysis above, here are key questions to consider:
1. **Which technical challenges are most concerning?**
- CORS configuration and authentication?
- Reverse proxy setup and routing?
- Build pipeline changes?
- Backward compatibility?
2. **What's the priority for implementation?**
- Quick wins (Solution A already done)
- Medium-term separation (Solution B)
- Long-term architectural evolution
3. **Which specific technical areas need more research?**
- Spring Boot static resource conditional disabling
- Vue.js runtime configuration vs build-time configuration
- Docker networking and service discovery
- Health checks and monitoring in separated architecture
4. **How should we handle the transition?**
- Feature flags and gradual rollout?
- Parallel support for both architectures?
- Big-bang switch for new deployments only?
### Next Steps for Brainstorming
The technical implementation details have been explored in depth. Before proceeding with any implementation, we need to decide on:
- **Goal**: What problem are we really trying to solve? (Build speed vs architectural separation)
- **Scope**: How much change is acceptable? (Minimal vs comprehensive)
- **Timeline**: When would this need to be implemented?
What aspect of the technical implementation would you like to dive deeper into, or are there other solutions you'd like to explore?

View file

@ -1,16 +0,0 @@
[![Open Collective backers and sponsors](https://img.shields.io/opencollective/all/komga?label=OpenCollective%20Sponsors&color=success)](https://opencollective.com/komga) ![GitHub Sponsors](https://img.shields.io/github/sponsors/gotson?label=Github%20Sponsors&color=success)
[![Discord](https://img.shields.io/discord/678794935368941569?label=Discord&color=blue)](https://discord.gg/TdRpkDu)
[![GitHub Workflow Status](https://img.shields.io/github/actions/workflow/status/gotson/komga/ci.yml?branch=master)](https://github.com/gotson/komga/actions?query=workflow%3ACI+branch%3Amaster)
[![GitHub release (latest SemVer)](https://img.shields.io/github/v/release/gotson/komga?color=blue&label=download&sort=semver)](https://github.com/gotson/komga/releases)
[![Docker Pulls](https://img.shields.io/docker/pulls/gotson/komga)](https://hub.docker.com/r/gotson/komga)
[![Translation status](https://hosted.weblate.org/widgets/komga/-/webui/svg-badge.svg)](https://hosted.weblate.org/engage/komga/)
# ![app icon](https://github.com/gotson/komga/raw/master/.github/readme-images/app-icon.png) Komga
[Komga](https://github.com/gotson/komga) is a media server for your comics, mangas, BDs, magazines and eBooks.
## Usage
Please refer to the [official documentation](https://komga.org/docs/installation/docker).

View file

@ -0,0 +1,71 @@
# Multi-stage optimized Docker build for Komga
# Frontend and backend are built in separate stages for better caching
# Stage 1: Frontend builder
FROM node:18-alpine AS frontend-builder
WORKDIR /app
# Copy package files first for better layer caching
COPY komga-webui/package*.json ./
# Install dependencies (use npm ci for reproducible builds)
RUN npm ci
# Copy the rest of the frontend source
COPY komga-webui/ ./
# Build the frontend
RUN npm run build
# Inject ThymeLeaf th: tags into index.html (mimics Gradle's prepareThymeLeaf task)
# This regex matches src="/path", href="/path", content="/path" and adds th: attributes
RUN sed -i -E 's/(src|href|content)="(\/[^"]*)"/\1="\2" th:\1="@{\2}"/g' dist/index.html
# Stage 2: Backend builder
FROM eclipse-temurin:21-jdk AS backend-builder
WORKDIR /app
# Install Node.js 18 (required for Gradle's npm tasks, though we won't run them)
RUN apt-get update && \
apt-get install -y curl && \
curl -fsSL https://deb.nodesource.com/setup_18.x | bash - && \
apt-get install -y nodejs && \
apt-get clean && rm -rf /var/lib/apt/lists/*
# Copy Gradle files
COPY gradle ./gradle
COPY gradlew ./
COPY gradle.properties ./
COPY settings.gradle ./
COPY build.gradle.kts ./
# Copy backend source (excluding webui which is already built)
COPY komga ./komga
COPY komga-tray ./komga-tray
# Copy built frontend from frontend-builder stage
COPY --from=frontend-builder /app/dist/ komga/src/main/resources/public/
# Build the backend JAR (skip prepareThymeLeaf since frontend is already built)
ENV NODE_OPTIONS="--max-old-space-size=4096"
RUN ./gradlew :komga:bootJar -x test -x generateGitProperties -x prepareThymeLeaf
# Stage 3: Runtime
FROM eclipse-temurin:21-jre-alpine
WORKDIR /app
# Copy built artifact from backend-builder stage
COPY --from=backend-builder /app/komga/build/libs/komga-*.jar komga.jar
# Create non-root user
RUN addgroup -S komga && adduser -S komga -G komga
USER komga
EXPOSE 25600
ENTRYPOINT ["java", "-Dspring.profiles.include=docker", "--enable-native-access=ALL-UNNAMED", "-jar", "komga.jar"]

2
Dockerfile.test-minimal Normal file
View file

@ -0,0 +1,2 @@
FROM alpine:latest
RUN echo "Test build successful"

5
Dockerfile.test-partial Normal file
View file

@ -0,0 +1,5 @@
FROM node:18-alpine AS frontend-builder
WORKDIR /app
COPY komga-webui/package*.json ./
RUN echo "Package files copied successfully"
RUN npm ci 2>&1 | tail -20

View file

@ -0,0 +1,137 @@
# Hướng dẫn Push Docker Images lên GitHub Container Registry
## Tổng quan
Hiện tại, dự án Komga fork sử dụng JReleaser để tự động build và push Docker images lên GitHub Container Registry (`ghcr.io/simplesoft-duongdt3/komga`) khi có release. Tài liệu này hướng dẫn cách để bạn tự push images lên repository của riêng bạn trên GitHub Container Registry.
Có hai phương pháp:
1. **Phương pháp thủ công**: Build image, tag và push bằng lệnh Docker trực tiếp lên GitHub Container Registry.
2. **Phương pháp sử dụng JReleaser**: Cấu hình JReleaser để push tự động lên GitHub Container Registry.
## Phương pháp thủ công
### Bước 1: Build Docker image
Sử dụng script `build-local-docker.sh` để build image local với version hiện tại (lấy từ `gradle.properties`):
```bash
./build-local-docker.sh
```
Script này sẽ build image với tag `komga-local:<version>`. Ví dụ: `komga-local:1.24.3`.
Nếu bạn muốn build với Dockerfile tối ưu (mặc định), script sử dụng `Dockerfile.local.optimized`. Bạn cũng có thể build trực tiếp bằng lệnh docker:
```bash
docker build -f Dockerfile.local.optimized -t komga-local:$(grep 'version=' gradle.properties | cut -d'=' -f2) .
```
### Bước 2: Tag image với repository đích
Để push lên GitHub Container Registry (thay `simplesoft-duongdt3` bằng tên tổ chức hoặc username GitHub):
```bash
docker tag komga-local:<version> ghcr.io/simplesoft-duongdt3/komga:<version>
docker tag komga-local:<version> ghcr.io/simplesoft-duongdt3/komga:latest
```
### Bước 3: Đăng nhập vào registry
**GitHub Container Registry:**
```bash
echo $GHCR_TOKEN | docker login ghcr.io -u simplesoft-duongdt3 --password-stdin
```
Trong đó `GHCR_TOKEN` là GitHub Personal Access Token với quyền `write:packages`. Bạn có thể tạo token tại https://github.com/settings/tokens.
### Bước 4: Push images
**Push lên GitHub Container Registry:**
```bash
docker push simplesoft-duongdt3/komga:<version>
docker push simplesoft-duongdt3/komga:latest
```
**Push lên GitHub Container Registry:**
```bash
docker push ghcr.io/simplesoft-duongdt3/komga:<version>
docker push ghcr.io/simplesoft-duongdt3/komga:latest
```
### Bước 5: Kiểm tra
Kiểm tra images đã được push lên GitHub Container Registry hoặc GHCR qua giao diện web hoặc lệnh:
```bash
docker pull simplesoft-duongdt3/komga:latest
```
## Phương pháp sử dụng JReleaser
JReleaser đã được cấu hình sẵn trong `build.gradle.kts` để build multiarch images (linux/amd64, linux/arm/v7, linux/arm64/v8) và push lên GitHub Container Registry và GHCR. Bạn có thể sửa cấu hình để thay đổi repository đích.
### Cấu hình JReleaser
Mở file `build.gradle.kts` và tìm phần `packagers.docker`. Sửa các mục sau:
1. **Thay đổi image names:** Tìm khối `imageNames` và thay thế bằng tên repository của bạn:
```kotlin
imageNames =
listOf(
"simplesoft-duongdt3/komga:latest",
"simplesoft-duongdt3/komga:{{projectVersion}}",
"simplesoft-duongdt3/komga:{{projectVersionMajor}}.x",
)
```
Để push cùng lúc cả GitHub Container Registry và GHCR, bạn có thể thêm nhiều image names hoặc cấu hình thêm registry.
2. **Cấu hình registries:** Phần `registries` đã định nghĩa `ghcr.io``ghcr.io`. Nếu bạn muốn push lên GitHub Container Registry với username khác, bạn cần đảm bảo đăng nhập qua `docker login` trước khi chạy JReleaser. JReleaser sử dụng `externalLogin = true`, nghĩa là nó sẽ sử dụng thông tin đăng nhập từ Docker CLI.
Nếu bạn chỉ muốn push lên một registry, bạn có thể xóa registry không cần thiết.
3. **Label source:** Nếu muốn thay đổi label `org.opencontainers.image.source` trong image, bạn cần sửa file template `komga/docker/Dockerfile.tpl` (dòng cuối).
### Chạy JReleaser publish
Sau khi cấu hình, bạn có thể chạy lệnh sau để build và push images:
```bash
./gradlew jreleaserPublish
```
Lệnh này sẽ:
- Build multiarch images sử dụng Docker Buildx.
- Push images lên các registry đã cấu hình.
**Lưu ý:** JReleaser publish thường được kích hoạt trong workflow release khi `docker_release: true`. Bạn có thể chạy local nhưng cần đảm bảo đã login vào registry tương ứng.
### Cấu hình qua environment variables
JReleaser cho phép override một số cấu hình qua biến môi trường, ví dụ:
- `JRELEASER_DOCKER_IMAGENAMES`: danh sách image names phân tách bằng dấu phẩy.
- `JRELEASER_DOCKER_REGISTRIES`: danh sách registries.
Tuy nhiên, cách đơn giản nhất là sửa file `build.gradle.kts`.
## Lưu ý quan trọng
1. **Version:** Version được lấy từ `gradle.properties`. Đảm bảo version đúng trước khi push.
2. **Multiarch:** Nếu bạn muốn hỗ trợ nhiều kiến trúc (amd64, arm64, arm/v7), hãy sử dụng JReleaser hoặc Docker Buildx. Phương pháp thủ công chỉ build image cho kiến trúc hiện tại.
3. **Quyền riêng tư:** Repository trên GitHub Container Registry và GHCR có thể là public hoặc private. Với private repository, bạn cần đăng nhập với token có quyền push.
4. **Dung lượng:** Image size khoảng 200300MB. Đảm bảo bạn có đủ quota trên registry.
## Troubleshooting
- **Lỗi unauthorized:** Kiểm tra lại đăng nhập với `docker login`. Với GHCR, token cần có quyền `write:packages`.
- **Lỗi denied: requested access to the resource is denied:** Đảm bảo tên repository đúng định dạng `username/reponame` và bạn có quyền push.
- **Lỗi manifest invalid:** Khi push multiarch images, cần sử dụng Docker Buildx. JReleaser đã tích hợp sẵn.
- **Lỗi không tìm thấy Dockerfile:** Khi dùng JReleaser, template Dockerfile nằm ở `komga/docker/Dockerfile.tpl`. Đảm bảo file tồn tại.
## Tham khảo
- [Docker Documentation](https://docs.docker.com/)
- [GitHub Container Registry Documentation](https://docs.github.com/en/packages/working-with-a-github-packages-registry/working-with-the-container-registry)
- [JReleaser Docker Packager](https://jreleaser.org/guide/latest/packagers/docker.html)

View file

@ -3,6 +3,8 @@
## Overview
Docker Compose setup để chạy Komga với PostgreSQL cho development và testing.
**Optimized Build**: Để build nhanh hơn với caching tốt hơn, sử dụng `Dockerfile.local.optimized``docker-compose.local.optimized.yml`.
## File Structure
### 1. docker-compose.yml (Production/Standard)
@ -115,7 +117,69 @@ volumes:
- Có thể timeout do download Gradle distribution
- Migration fixes đã được áp dụng trong source code
### 3. docker-compose-test.yml (cho testing)
### 2a. docker-compose.local.optimized.yml (Local Development với Build Optimized)
```yaml
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: komga-postgres
environment:
POSTGRES_DB: komga
POSTGRES_USER: komga
POSTGRES_PASSWORD: komga123
ports:
- "5433:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/01-init.sql
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U komga" ]
interval: 10s
timeout: 5s
retries: 5
komga:
build:
context: .
dockerfile: Dockerfile.local.optimized
container_name: komga-backend
depends_on:
postgres:
condition: service_healthy
environment:
SPRING_PROFILES_ACTIVE: docker
KOMGA_DATABASE_TYPE: postgresql
KOMGA_DATABASE_URL: jdbc:postgresql://postgres:5432/komga?connectTimeout=30000&socketTimeout=60000
KOMGA_DATABASE_USERNAME: komga
KOMGA_DATABASE_PASSWORD: komga123
KOMGA_CONFIG_DIR: /config
KOMGA_DATABASE_POOL_SIZE: 10
KOMGA_DATABASE_MAX_POOL_SIZE: 10
SPRING_FLYWAY_ENABLED: "true"
SPRING_FLYWAY_BASELINE_ON_MIGRATE: "true"
SPRING_FLYWAY_BASELINE_VERSION: "20250730173126"
ports:
- "25600:25600"
volumes:
- komga_config:/config
- ./data:/data:ro
restart: unless-stopped
volumes:
postgres_data:
komga_config:
```
**Lưu ý về Dockerfile.local.optimized:**
- Multi-stage build tách biệt frontend và backend để tối ưu caching
- Frontend (`npm install`, `npm run build`) được cache độc lập
- Thay đổi backend không trigger rebuild frontend
- Build nhanh hơn đáng kể cho incremental builds
- Sử dụng: `docker-compose -f docker-compose.local.optimized.yml up -d`
### 4. docker-compose-test.yml (cho testing)
```yaml
version: '3.8'
@ -143,7 +207,7 @@ volumes:
postgres_test_data:
```
### 3. docker/postgres/init.sql
### 5. docker/postgres/init.sql
```sql
-- PostgreSQL initialization script for Komga
-- Creates necessary extensions and sets up database

View file

@ -0,0 +1,281 @@
# Frontend Build Process and Thymeleaf Integration
## Overview
Komga's frontend is built using Vue.js and integrated with the Spring Boot backend through Thymeleaf template processing. This document explains the build pipeline, Thymeleaf tag injection, and how static assets are served.
## Build Pipeline
### 1. Frontend Build (`komga-webui/`)
- **Technology Stack**: Vue.js 2.6 with TypeScript, Vuetify, Vuex, Vue Router
- **Build Tool**: Vue CLI with Webpack
- **Entry Point**: `komga-webui/src/main.ts`
- **Build Output**: `komga-webui/dist/` directory
### 2. Gradle Tasks (`komga/build.gradle.kts`)
The build process is orchestrated by Gradle tasks:
```kotlin
// 1. npmInstall - Install Node.js dependencies
register<Exec>("npmInstall") {
workingDir(webui) // $rootDir/komga-webui
commandLine("npm", "install")
}
// 2. npmBuild - Build frontend with Vue CLI
register<Exec>("npmBuild") {
dependsOn("npmInstall")
workingDir(webui)
commandLine("npm", "run", "build")
}
// 3. copyWebDist - Copy built files to resources
register<Sync>("copyWebDist") {
dependsOn("npmBuild")
from("$webui/dist/")
into("$projectDir/src/main/resources/public/")
}
// 4. prepareThymeLeaf - Inject Thymeleaf tags
register<Copy>("prepareThymeLeaf") {
dependsOn("copyWebDist")
from("$webui/dist/index.html")
into("$projectDir/src/main/resources/public/")
filter { line ->
line.replace("((?:src|content|href)=\")([\\w]*/.*?)(\")".toRegex()) {
it.groups[0]?.value + " th:" + it.groups[1]?.value + "@{" +
it.groups[2]?.value?.prefixIfNot("/") + "}" + it.groups[3]?.value
}
}
}
```
## Thymeleaf Tag Injection
### Purpose
Thymeleaf tags are injected to make static resource URLs context-aware. When Komga is deployed under a subpath (e.g., `http://example.com/komga/`), the tags ensure assets load correctly without hardcoded paths.
### Injected Tags in `index.html`
```html
<!-- Original (from Vue build) -->
<link rel="icon" href="/favicon.ico">
<script>window.resourceBaseUrl = '/'</script>
<!-- After prepareThymeLeaf task -->
<link rel="icon" href="/favicon.ico" th:href="@{/favicon.ico}">
<script th:inline="javascript">
window.resourceBaseUrl = /*[(${"'" + baseUrl + "'"})]*/ '/'
</script>
```
### Tag Breakdown
1. **URL Rewriting Tags**:
```html
th:href="@{/favicon.ico}"
th:content="@{/mstile-144x144.png}"
th:href="@{/manifest.json}"
```
- `@{...}` syntax: Automatically prepends context path
- Works for `href`, `src`, `content` attributes
- Applies to: favicons, manifest, tile images
2. **JavaScript Inlining**:
```html
<script th:inline="javascript">
window.resourceBaseUrl = /*[(${"'" + baseUrl + "'"})]*/ '/'
</script>
```
- `th:inline="javascript"`: Enables Thymeleaf processing in JS
- `/*[(${...})]*/`: Injects server-side `baseUrl` variable
- `window.resourceBaseUrl`: Used by frontend for API calls
### Regex Pattern
The `prepareThymeLeaf` task uses this regex to find and replace URLs:
```regex
((?:src|content|href)=\")([\\w]*/.*?)(\")
```
- Matches: `src="...`, `content="...`, `href="...`
- Groups: Attribute name and URL value
- Replacement: Adds `th:` prefix and `@{...}` wrapper
## Frontend Distribution (`frontend-dist/`)
### Directory Structure
```
frontend-dist/
├── index.html # Main HTML with Thymeleaf tags
├── js/
│ ├── app.cd1e7444.js # Main application bundle
│ ├── chunk-vendors.215f0888.js # Vendor bundle
│ └── [chunk].js # Code-split chunks
├── css/
│ ├── app.c113e1ad.css # Application styles
│ └── chunk-vendors.420551a9.css # Vendor styles
├── fonts/ # Web fonts
├── img/ # Static images
└── manifest.json # PWA manifest
```
### Building for Distribution
The `build-local-docker.sh` script can export frontend files:
```bash
./build-local-docker.sh --export-dist ./frontend-dist
```
- Uses Docker multi-stage build to extract dist files
- Creates standalone frontend distribution
- Useful for testing or manual deployment
## Integration with Backend
### 1. Static Resource Serving
- **Location**: `komga/src/main/resources/public/`
- **Spring Boot**: Automatically serves files from `classpath:/public/`
- **Access**: `http://localhost:25600/` serves `index.html`
### 2. Thymeleaf Controller
```java
// Komga uses Thymeleaf for serving index.html
// The controller maps root path to index.html
```
### 3. Base URL Handling
- **Server-side**: `baseUrl` is determined from request context
- **Client-side**: `window.resourceBaseUrl` used for API calls
- **API Proxy**: Frontend routes API requests through correct path
## Development vs Production
### Development Mode
- **Vue CLI Dev Server**: `npm run serve` on port 8081
- **Hot Reload**: Instant updates without full rebuild
- **API Proxy**: Dev server proxies API calls to backend (port 25600)
### Production Build
- **Optimized Bundles**: Minified JS, CSS with hash filenames
- **Code Splitting**: Dynamic imports for smaller initial load
- **Tree Shaking**: Unused code removed
## Testing
### Unit Tests
```bash
cd komga-webui
npm run test:unit
```
- **Framework**: Jest with Vue Test Utils
- **Location**: `komga-webui/tests/unit/`
- **Coverage**: Component functions and utilities
### Linting
```bash
npm run lint
```
- **Tools**: ESLint with Vue/TypeScript rules
- **Configuration**: `.eslintrc.js`
- **Pre-commit**: Runs automatically in CI
## Common Issues and Solutions
### 1. Missing Thymeleaf Tags
**Symptom**: Assets fail to load when deployed under subpath
**Solution**: Ensure `prepareThymeLeaf` task runs before `bootJar`
```bash
./gradlew :komga:prepareThymeLeaf :komga:bootJar
```
### 2. Cached Assets with Hash Names
**Symptom**: Browser caches old JS/CSS files
**Solution**: Webpack hash filenames (e.g., `app.cd1e7444.js`) bust cache automatically
### 3. Base URL Incorrect
**Symptom**: API calls go to wrong path
**Debug**: Check `window.resourceBaseUrl` in browser console
**Fix**: Verify Thymeleaf `baseUrl` injection
### 4. Docker Build Issues
**Symptom**: Frontend not updated in Docker image
**Solution**: Use optimized Docker build with separate frontend stage
```bash
./build-local-docker.sh --export-dist
```
## Best Practices
1. **Always run full build chain**:
```bash
./gradlew :komga:prepareThymeLeaf :komga:bootJar
```
2. **Test with subpath deployment**:
```bash
# Deploy to /komga context
server.servlet.context-path=/komga
```
3. **Monitor bundle sizes**:
- Check `frontend-dist/js/` directory size
- Use Webpack Bundle Analyzer if needed
4. **Keep Thymeleaf tags minimal**:
- Only inject where context-awareness needed
- Avoid performance overhead
## Local Development Setup
### Quick Start with Pre-built Frontend
When you have a pre-built frontend in `frontend-dist/` (e.g., from Docker export or previous build), follow these steps to integrate with the Spring Boot backend:
1. **Copy static assets to public directory**:
```bash
mkdir -p komga/src/main/resources/public
cp -r frontend-dist/* komga/src/main/resources/public/
```
2. **Move index.html to templates directory** (required for Thymeleaf processing):
```bash
mkdir -p komga/src/main/resources/templates
mv komga/src/main/resources/public/index.html komga/src/main/resources/templates/
```
3. **Start backend excluding frontend build tasks**:
```bash
./gradlew :komga:bootRun -x npmInstall -x npmBuild -x copyWebDist -x prepareThymeLeaf
```
4. **Verify frontend is served**:
- Open http://localhost:25600/
- Check browser console for errors
- Static assets should load (JS, CSS, fonts)
### Using PostgreSQL (Optional)
If you need PostgreSQL for development, start the database first:
```bash
docker-compose -f docker-compose.local.yml up -d postgres
```
Then set environment variables or activate `postgresql` profile.
### Troubleshooting
**Thymeleaf template error "Error resolving template [index]"**:
- Ensure `index.html` is in `komga/src/main/resources/templates/`
- Remove duplicate `index.html` from `public/` directory
**Static assets not loading (404)**:
- Verify JS/CSS files exist in `komga/src/main/resources/public/`
- Check that Thymeleaf tags are properly injected (view page source)
- Ensure `baseUrl` is correctly set (should be `/` for local development)
**Database errors during startup**:
- Use SQLite default by not setting any database environment variables
- Or ensure PostgreSQL is running and schema is applied
## Related Files
- `komga/build.gradle.kts`: Gradle build configuration
- `komga-webui/vue.config.js`: Vue CLI configuration
- `komga-webui/package.json`: Frontend dependencies
- `build-local-docker.sh`: Docker build script
- `frontend-dist/index.html`: Final HTML with Thymeleaf tags

View file

@ -3,26 +3,95 @@
# Build Komga Docker image locally with multi-stage build
set -e
# Default to optimized build
OPTIMIZED=true
DOCKERFILE="Dockerfile.local.optimized"
BUILD_TYPE="Optimized multi-stage"
EXPORT_DIST=false
EXPORT_DIST_DIR="./frontend-dist"
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
--no-optimized)
OPTIMIZED=false
DOCKERFILE="Dockerfile.local"
BUILD_TYPE="Standard"
shift
;;
--export-dist)
EXPORT_DIST=true
if [[ -n "$2" && "$2" != --* ]]; then
EXPORT_DIST_DIR="$2"
shift
else
EXPORT_DIST_DIR="./frontend-dist"
fi
shift
;;
--help|-h)
echo "Usage: $0 [--no-optimized] [--export-dist [DIR]]"
echo ""
echo "Build Komga Docker image locally."
echo ""
echo "Options:"
echo " --no-optimized Use the standard Dockerfile.local instead of optimized version"
echo " --export-dist [DIR] Export frontend dist files to directory (default: ./frontend-dist)"
echo " --help, -h Show this help message"
echo ""
echo "By default, uses Dockerfile.local.optimized for better build caching."
exit 0
;;
*)
echo "Unknown option: $1"
echo "Use --help for usage information"
exit 1
;;
esac
done
echo "Building Komga Docker image locally..."
echo "Build type: $BUILD_TYPE"
echo "Dockerfile: $DOCKERFILE"
echo ""
# Get version from gradle.properties
VERSION=$(grep 'version=' gradle.properties | cut -d'=' -f2)
echo "Building version: $VERSION"
# Build Docker image with multi-stage build (Node.js 18 + Java 21)
docker build -f Dockerfile.local -t komga-local:$VERSION .
# Build Docker image
docker build -f $DOCKERFILE -t komga-local:$VERSION .
# Export frontend dist files if requested
if [ "$EXPORT_DIST" = true ]; then
if [ "$OPTIMIZED" = true ]; then
echo "Exporting frontend dist files to $EXPORT_DIST_DIR"
mkdir -p "$EXPORT_DIST_DIR"
# Build frontend-builder stage and extract dist files
docker build --target frontend-builder -t komga-frontend-builder-$VERSION -f $DOCKERFILE .
docker run --rm komga-frontend-builder-$VERSION tar -czf - -C /app/dist . | tar -xzf - -C "$EXPORT_DIST_DIR"
echo "Dist files exported to $EXPORT_DIST_DIR"
docker rmi komga-frontend-builder-$VERSION 2>/dev/null || true
else
echo "Export dist is only supported with optimized build (--no-optimized not supported)"
fi
fi
echo ""
echo "Docker image built successfully:"
echo " Image: komga-local:$VERSION"
echo " Build: Multi-stage (Node.js 18 + Java 21)"
echo " Build: $BUILD_TYPE (Node.js 18 + Java 21)"
echo " Runtime: Java 21 (Temurin JRE)"
echo ""
echo "To run standalone container:"
echo " docker run -p 25600:25600 -v /path/to/config:/config komga-local:$VERSION"
echo ""
echo "To run with docker-compose:"
echo " docker-compose up -d"
if [ "$OPTIMIZED" = true ]; then
echo " docker-compose -f docker-compose.local.optimized.yml up -d"
else
echo " docker-compose -f docker-compose.local.yml up -d"
fi
echo ""
echo "To tag with different name:"
echo " docker tag komga-local:$VERSION komga:latest"

View file

@ -167,15 +167,14 @@ jreleaser {
continueOnError = false
templateDirectory = rootDir.resolve("komga/docker")
repository.active = Active.NEVER
buildArgs = listOf("--cache-from", "gotson/komga:latest")
imageNames =
listOf(
"komga:latest",
"komga:{{projectVersion}}",
"komga:{{projectVersionMajor}}.x",
"simplesoft-duongdt3/komga:latest",
"simplesoft-duongdt3/komga:{{projectVersion}}",
"simplesoft-duongdt3/komga:{{projectVersionMajor}}.x",
)
registries {
create("docker.io") { externalLogin = true }
create("ghcr.io") { externalLogin = true }
}
buildx {

View file

@ -4,7 +4,7 @@ include required("/stdlib/jdk/23/eclipse.conf")
app {
display-name = Komga
fsname = komga
vcs-url = "https://github.com/gotson/komga"
vcs-url = "https://github.com/simplesoft-duongdt3/komga"
vendor = "Gotson"
description = "Media server for comics/mangas/BDs with API and OPDS support"
license = MIT

View file

@ -21,7 +21,7 @@ services:
command: ["postgres", "-c", "log_statement=all"] # Log all SQL for debugging
komga-test:
image: gotson/komga:latest
image: ghcr.io/simplesoft-duongdt3/komga:latest
container_name: komga-test-backend
depends_on:
postgres-test:

View file

@ -0,0 +1,51 @@
version: '3.8'
services:
postgres:
image: postgres:15-alpine
container_name: komga-postgres
environment:
POSTGRES_DB: komga
POSTGRES_USER: komga
POSTGRES_PASSWORD: komga123
ports:
- "5433:5432"
volumes:
- postgres_data:/var/lib/postgresql/data
- ./docker/postgres/init.sql:/docker-entrypoint-initdb.d/01-init.sql
healthcheck:
test: [ "CMD-SHELL", "pg_isready -U komga" ]
interval: 10s
timeout: 5s
retries: 5
komga:
build:
context: .
dockerfile: Dockerfile.local.optimized
container_name: komga-backend
depends_on:
postgres:
condition: service_healthy
environment:
SPRING_PROFILES_ACTIVE: docker
KOMGA_DATABASE_TYPE: postgresql
KOMGA_DATABASE_URL: jdbc:postgresql://postgres:5432/komga?connectTimeout=30000&socketTimeout=60000
KOMGA_DATABASE_USERNAME: komga
KOMGA_DATABASE_PASSWORD: komga123
KOMGA_CONFIG_DIR: /config
KOMGA_DATABASE_POOL_SIZE: 10
KOMGA_DATABASE_MAX_POOL_SIZE: 10
SPRING_FLYWAY_ENABLED: "true"
SPRING_FLYWAY_BASELINE_ON_MIGRATE: "true"
SPRING_FLYWAY_BASELINE_VERSION: "20250730173126"
ports:
- "25600:25600"
volumes:
- komga_config:/config
- ./data:/data:ro
restart: unless-stopped
volumes:
postgres_data:
komga_config:

View file

@ -20,7 +20,7 @@ services:
retries: 5
komga:
image: komga-local:1.24.3.2026060801
image: komga-local:1.24.3
container_name: komga-backend
depends_on:
postgres:

View file

@ -53,4 +53,4 @@ ENV KOMGA_CONFIGDIR="/config"
ENV LANG='en_US.UTF-8' LANGUAGE='en_US:en' LC_ALL='en_US.UTF-8'
ENTRYPOINT ["java", "-Dspring.profiles.include=docker", "--enable-native-access=ALL-UNNAMED", "-jar", "application.jar", "--spring.config.additional-location=file:/config/"]
EXPOSE 25600
LABEL org.opencontainers.image.source="https://github.com/gotson/komga"
LABEL org.opencontainers.image.source="https://github.com/simplesoft-duongdt3/komga"

View file

@ -90,7 +90,7 @@ class OpenApiConfiguration(
Check the API reference:
- on the [Komga website](https://komga.org/docs/openapi/komga-api)
- on any running Komga instance at `/swagger-ui.html`
- on [GitHub](https://raw.githubusercontent.com/gotson/komga/refs/heads/master/komga/docs/openapi.json)
- on [GitHub](https://raw.githubusercontent.com/simplesoft-duongdt3/komga/refs/heads/master/komga/docs/openapi.json)
## Authentication
@ -119,7 +119,7 @@ class OpenApiConfiguration(
API endpoints marked as deprecated will be removed in the next major version.
""".trimIndent(),
).license(License().name("MIT").url("https://github.com/gotson/komga/blob/master/LICENSE")),
).license(License().name("MIT").url("https://github.com/simplesoft-duongdt3/komga/blob/master/LICENSE")),
).externalDocs(
ExternalDocumentation()
.description("Komga documentation")

View file

@ -116,7 +116,7 @@ class OpdsController(
@Qualifier("pdfImageType")
private val pdfImageType: ImageType,
) {
private val komgaAuthor = OpdsAuthor("Komga", URI("https://github.com/gotson/komga"))
private val komgaAuthor = OpdsAuthor("Komga", URI("https://github.com/simplesoft-duongdt3/komga"))
private val decimalFormat = DecimalFormat("0.#")

View file

@ -19,7 +19,7 @@ import org.springframework.web.reactive.function.client.WebClient
import org.springframework.web.server.ResponseStatusException
import java.util.concurrent.TimeUnit
private const val GITHUB_API = "https://api.github.com/repos/gotson/komga/releases"
private const val GITHUB_API = "https://api.github.com/repos/simplesoft-duongdt3/komga/releases"
@RestController
@PreAuthorize("hasRole('ADMIN')")

74
start-dev-with-frontend.sh Executable file
View file

@ -0,0 +1,74 @@
#!/bin/bash
# Start Komga backend with pre-built frontend for local development
set -e
FRONTEND_SOURCE="./frontend-dist"
BACKEND_RESOURCES="./komga/src/main/resources"
echo "=== Komga Local Development with Frontend ==="
echo "Frontend source: $FRONTEND_SOURCE"
# Check if frontend dist exists
if [ ! -d "$FRONTEND_SOURCE" ]; then
echo "ERROR: Frontend distribution not found at $FRONTEND_SOURCE"
echo "Build frontend first: ./build-local-docker.sh --export-dist"
exit 1
fi
# Ensure backend resources directories exist
mkdir -p "$BACKEND_RESOURCES/public"
mkdir -p "$BACKEND_RESOURCES/templates"
echo "Copying static assets to $BACKEND_RESOURCES/public/"
rsync -a --exclude="index.html" "$FRONTEND_SOURCE/" "$BACKEND_RESOURCES/public/" 2>/dev/null || cp -r "$FRONTEND_SOURCE/"* "$BACKEND_RESOURCES/public/" 2>/dev/null
echo "Moving index.html to $BACKEND_RESOURCES/templates/"
if [ -f "$BACKEND_RESOURCES/public/index.html" ]; then
mv "$BACKEND_RESOURCES/public/index.html" "$BACKEND_RESOURCES/templates/"
elif [ -f "$FRONTEND_SOURCE/index.html" ]; then
cp "$FRONTEND_SOURCE/index.html" "$BACKEND_RESOURCES/templates/"
fi
# Check if PostgreSQL option is provided
USE_POSTGRES=false
if [[ "$1" == "--postgres" ]]; then
USE_POSTGRES=true
echo "Using PostgreSQL database"
fi
if [ "$USE_POSTGRES" = true ]; then
echo "Starting PostgreSQL container..."
docker-compose -f docker-compose.local.yml up -d postgres
echo "Waiting for PostgreSQL to be ready..."
until docker exec komga-postgres pg_isready -U komga -d komga; do
sleep 1
done
echo "Setting PostgreSQL environment variables..."
export KOMGA_DATABASE_DRIVER_CLASS_NAME=org.postgresql.Driver
export KOMGA_DATABASE_URL="jdbc:postgresql://localhost:5433/komga?user=komga&password=komga123"
export KOMGA_DATABASE_USERNAME=komga
export KOMGA_DATABASE_PASSWORD=komga123
export KOMGA_DATABASE_TYPE=postgresql
export KOMGA_DATABASE_POOL_SIZE=10
export KOMGA_DATABASE_MAX_POOL_SIZE=10
export SPRING_FLYWAY_BASELINE_ON_MIGRATE=true
export SPRING_FLYWAY_BASELINE_VERSION=20250730173126
export KOMGA_CONFIG_DIR="$HOME/.komga-postgres"
export SPRING_PROFILES_ACTIVE=postgresql
fi
echo "Starting Komga backend..."
echo "Note: Frontend build tasks are skipped"
echo "Access at: http://localhost:25600/"
echo "Press Ctrl+C to stop"
# Run backend with excluded frontend tasks
./gradlew :komga:bootRun -x npmInstall -x npmBuild -x copyWebDist -x prepareThymeLeaf
# Cleanup on exit
if [ "$USE_POSTGRES" = true ]; then
echo "Stopping PostgreSQL container..."
docker-compose -f docker-compose.local.yml stop postgres
fi